dsh-github-flow 0.1.7 → 0.2.0

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 (65) hide show
  1. package/README.en.md +287 -69
  2. package/README.md +290 -68
  3. package/docs/architecture.md +71 -14
  4. package/docs/images/1.png +0 -0
  5. package/docs/images/2.png +0 -0
  6. package/docs/images/3.png +0 -0
  7. package/docs/images/4.png +0 -0
  8. package/docs/npm-publish-guide.md +12 -0
  9. package/lib/api/route-helpers.d.ts +47 -0
  10. package/lib/api/route-helpers.js +134 -0
  11. package/lib/api/routes-actions.d.ts +9 -0
  12. package/lib/api/routes-actions.js +115 -0
  13. package/lib/api/routes-lifecycle.d.ts +9 -0
  14. package/lib/api/routes-lifecycle.js +75 -0
  15. package/lib/api/routes-overview.d.ts +11 -0
  16. package/lib/api/routes-overview.js +227 -0
  17. package/lib/api/routes-proxy.d.ts +18 -0
  18. package/lib/api/routes-proxy.js +63 -0
  19. package/lib/api/routes.d.ts +17 -5
  20. package/lib/api/routes.js +18 -417
  21. package/lib/client.js +1179 -40
  22. package/lib/commands/gh.d.ts +2 -1
  23. package/lib/commands/gh.js +40 -3
  24. package/lib/executor.d.ts +17 -0
  25. package/lib/executor.js +40 -2
  26. package/lib/main.d.ts +12 -0
  27. package/lib/main.js +73 -4
  28. package/lib/proxy.d.ts +87 -0
  29. package/lib/proxy.js +275 -0
  30. package/lib/renderers/markdown.d.ts +24 -0
  31. package/lib/renderers/markdown.js +493 -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 +212 -0
  36. package/lib/tools/issue.d.ts +32 -1
  37. package/lib/tools/issue.js +94 -17
  38. package/lib/tools/pr.d.ts +22 -1
  39. package/lib/tools/pr.js +67 -15
  40. package/lib/tools/release.d.ts +89 -0
  41. package/lib/tools/release.js +219 -0
  42. package/lib/tools/repo.d.ts +13 -1
  43. package/lib/tools/repo.js +46 -14
  44. package/lib/tools/run.d.ts +22 -1
  45. package/lib/tools/run.js +74 -14
  46. package/lib/tools/secret.d.ts +60 -0
  47. package/lib/tools/secret.js +175 -0
  48. package/lib/tools/skill.d.ts +63 -0
  49. package/lib/tools/skill.js +146 -0
  50. package/lib/types.d.ts +57 -0
  51. package/lib/updater.d.ts +49 -0
  52. package/lib/updater.js +169 -0
  53. package/lib/upgrade-manager.d.ts +58 -0
  54. package/lib/upgrade-manager.js +309 -0
  55. package/lib/version.d.ts +13 -0
  56. package/lib/version.js +13 -0
  57. package/package.json +3 -2
  58. package/lib/client/icons.d.ts +0 -13
  59. package/lib/client/icons.js +0 -41
  60. package/lib/client/index.d.ts +0 -3
  61. package/lib/client/index.js +0 -36
  62. package/lib/client/panel.d.ts +0 -1
  63. package/lib/client/panel.js +0 -102
  64. package/lib/client/style.d.ts +0 -1
  65. package/lib/client/style.js +0 -555
@@ -1,2 +1,3 @@
1
1
  import type { GhExecutor } from '../executor.js';
2
- export declare function registerGhCommand(commandsService: any, executor: GhExecutor): void;
2
+ import type { UpdateChecker } from '../updater.js';
3
+ export declare function registerGhCommand(commandsService: any, executor: GhExecutor, updateChecker?: UpdateChecker): void;
@@ -1,4 +1,5 @@
1
- export function registerGhCommand(commandsService, executor) {
1
+ import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL } from '../version.js';
2
+ export function registerGhCommand(commandsService, executor, updateChecker) {
2
3
  if (!commandsService || typeof commandsService.register !== 'function')
3
4
  return;
4
5
  commandsService.register({
@@ -11,7 +12,14 @@ export function registerGhCommand(commandsService, executor) {
11
12
  // 「我刚 gh auth login 到底成没成」,给他 30 秒前的缓存等于答非所问。
12
13
  const auth = await executor.checkAuth(undefined, true);
13
14
  const repo = await executor.getRepoMetadata();
15
+ const updateInfo = updateChecker?.getQuickStatus();
14
16
  let md = `### GitHub 集成状态\n\n`;
17
+ // 若检测到新版本,在最上方展示醒目的升级指引横幅
18
+ if (updateInfo?.hasUpdate) {
19
+ md += `> 🚀 **发现新版本 v${updateInfo.latestVersion}**(当前为 \`v${updateInfo.currentVersion}\`)\n`;
20
+ md += `> 输入 \`/gh upgrade\` 查看升级指引,或在终端执行:\`${updateInfo.upgradeCommand}\`\n\n`;
21
+ }
22
+ md += `- **插件版本**: \`v${updateInfo?.currentVersion || CURRENT_PLUGIN_VERSION}\`\n`;
15
23
  if (auth.loggedIn) {
16
24
  md += `- **登录账号**: \`${auth.user || '已登录'}\`\n`;
17
25
  md += `- **授权作用域**: ${auth.scopes?.map((s) => `\`${s}\``).join(', ') || '标准权限'}\n`;
@@ -35,15 +43,44 @@ export function registerGhCommand(commandsService, executor) {
35
43
  text: md,
36
44
  };
37
45
  }
46
+ if (trimmed === 'upgrade') {
47
+ const info = updateChecker ? await updateChecker.check(true) : null;
48
+ const curVer = info?.currentVersion || CURRENT_PLUGIN_VERSION;
49
+ const latestVer = info?.latestVersion || curVer;
50
+ const hasUpdate = info?.hasUpdate ?? false;
51
+ const isMac = process.platform === 'darwin';
52
+ const isWin = process.platform === 'win32';
53
+ const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
54
+ let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
55
+ md += `- **当前运行版本**: \`v${curVer}\`\n`;
56
+ md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
57
+ md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
58
+ md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
59
+ md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
60
+ md += `1. **DSH 官方标准工程升级(推荐)**:\n\`\`\`bash\npnpm dsh plugin add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
61
+ md += `2. **DSH 桌面版 Profile 环境升级**:\n\`\`\`bash\ndsh plugin --profile desktop add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n`;
62
+ if (isMac) {
63
+ md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
64
+ }
65
+ else {
66
+ md += `\n`;
67
+ }
68
+ md += `3. **Node 全局环境升级**:\n\`\`\`bash\nnpm install -g ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
69
+ md += `> 💡 **升级后提示**:Node.js 运行时对已载入模块存在强缓存,升级后**请重启 DSH 服务以完全加载最新后端工具**。`;
70
+ return {
71
+ kind: 'success',
72
+ text: md,
73
+ };
74
+ }
38
75
  if (trimmed === 'help') {
39
76
  return {
40
77
  kind: 'success',
41
- text: `### DSH GitHub Flow 帮助\n\n- \`/gh status\`: 检查当前 GitHub CLI 账号与仓库连接\n- \`/gh help\`: 显示此帮助信息\n\n可以在对话中直接让 Agent 操作 GitHub,如:\n- *"查看当前仓库有哪些开放的 PR"* \n- *"查看最近一次 CI 失败的原因"* \n- *"帮我给当前分支创建 PR"*`,
78
+ text: `### DSH GitHub Flow 帮助\n\n- \`/gh status\`: 检查当前 GitHub CLI 账号、仓库连接与插件版本\n- \`/gh upgrade\`: 检查新版本并显示全场景升级命令指引\n- \`/gh help\`: 显示此帮助信息\n\n可以在对话中直接让 Agent 操作 GitHub,如:\n- *"查看当前仓库有哪些开放的 PR"* \n- *"查看最近一次 CI 失败的原因"* \n- *"帮我给当前分支创建 PR"*`,
42
79
  };
43
80
  }
44
81
  return {
45
82
  kind: 'error',
46
- text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh help`,
83
+ text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh upgrade, /gh help`,
47
84
  };
48
85
  },
49
86
  });
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
@@ -95,6 +95,20 @@ export class GhExecutor {
95
95
  maxOutputChars;
96
96
  /** UTF-8 字节口径的硬上限;未配置时按 maxOutputChars × 4 推导(覆盖 CJK 3 字节与 emoji 4 字节) */
97
97
  maxOutputBytes;
98
+ /**
99
+ * 列表类查询的默认条数上限。
100
+ * 为什么挂在 executor 而不是给每个工具工厂加 config 参数:executor 是全部
101
+ * 工具共享的依赖注入点,配置语义本就属于「执行层默认值」;工厂签名零改动,
102
+ * 且热更新路径 updateConfig 能同步刷新。工具内以 `?? 20` 兜底保证旧配置兼容。
103
+ */
104
+ defaultListLimit;
105
+ /**
106
+ * 代理管理器引用(可选依赖)。
107
+ * 注入后每次 gh 子进程拉起都会合并代理环境变量——这是「工具调用 + Web 页面数据
108
+ * 全部走代理」的唯一咽喉(两条链路共用本 executor)。未注入时零干预,
109
+ * gh 自然继承宿主系统环境变量(系统级代理或直连),即「默认走系统设置」语义。
110
+ */
111
+ proxyManager;
98
112
  /** 认证状态缓存。`gh auth status` 实测 5.4 秒,而它位于全局驾驶舱关键路径的最前面 */
99
113
  authCache = null;
100
114
  constructor(config = {}) {
@@ -102,12 +116,15 @@ export class GhExecutor {
102
116
  this.defaultTimeoutMs = config.defaultTimeoutMs || 30_000;
103
117
  this.maxOutputChars = config.maxOutputChars || 24_000;
104
118
  this.maxOutputBytes = config.maxOutputBytes || this.maxOutputChars * 4;
119
+ this.defaultListLimit = config.defaultListLimit || 20;
105
120
  }
106
121
  updateConfig(config = {}) {
107
122
  if (config.ghPath)
108
123
  this.ghPath = config.ghPath;
109
124
  if (config.defaultTimeoutMs)
110
125
  this.defaultTimeoutMs = config.defaultTimeoutMs;
126
+ if (config.defaultListLimit)
127
+ this.defaultListLimit = config.defaultListLimit;
111
128
  if (config.maxOutputChars) {
112
129
  this.maxOutputChars = config.maxOutputChars;
113
130
  if (!config.maxOutputBytes)
@@ -116,6 +133,10 @@ export class GhExecutor {
116
133
  if (config.maxOutputBytes)
117
134
  this.maxOutputBytes = config.maxOutputBytes;
118
135
  }
136
+ /** 注入代理管理器(main.ts 装配时调用一次;测试可注入 mock) */
137
+ attachProxyManager(manager) {
138
+ this.proxyManager = manager;
139
+ }
119
140
  /**
120
141
  * 智能解析最可靠的工作区物理路径:
121
142
  * 1. 优先使用显式指定的有效 cwd(若包含 .git 目录)
@@ -173,8 +194,15 @@ export class GhExecutor {
173
194
  ...process.env,
174
195
  GH_NO_UPDATE_NOTIFIER: '1', // 禁用 CLI 版本更新检查,避免污染 stdout
175
196
  NO_COLOR: '1', // 禁用 ANSI 颜色转义字符
197
+ // 代理环境变量注入在 options.env 之前:调用方显式传入的 env 仍可覆盖
198
+ // 代理(保持 options.env 的既有语义优先级),正常调用方不传则代理生效。
199
+ ...(this.proxyManager?.getEnv('gh') || {}),
176
200
  ...options.env,
177
201
  },
202
+ // 仅在显式传入 stdin 时附加 input 键:promisified execFile 会把 input
203
+ // 写入子进程 stdin 后立即结束流。不传则保持既有行为(继承/null 句柄),
204
+ // 确保对全部既有调用方零破坏。
205
+ ...(options.stdin !== undefined ? { input: options.stdin } : {}),
178
206
  });
179
207
  let rawOutput = (stdout || '').trim();
180
208
  if (!rawOutput && stderr) {
@@ -269,7 +297,18 @@ export class GhExecutor {
269
297
  async probeAuth(cwd) {
270
298
  const res = await this.run(['auth', 'status'], { cwd, timeoutMs: 10_000, rawText: true });
271
299
  const raw = (res.rawOutput || '') + '\n' + (res.error || '');
272
- const loggedIn = raw.includes('Logged in to') || raw.includes('✓');
300
+ // 多重维度强健判定登录态 (容错 Windows Unicode 乱码、Keyring 告警与不同版本 CLI 输出差分)
301
+ const hasLoggedInFlag = /logged\s+in\s+to/i.test(raw) || raw.includes('✓') || raw.includes('√');
302
+ const hasActiveFlag = /active\s+account:\s*true/i.test(raw);
303
+ const hasTokenFlag = raw.includes('Token:') || raw.includes('Token scopes:');
304
+ const accountMatch = raw.match(/account\s+([A-Za-z0-9_-]+)/i);
305
+ const hasAccount = Boolean(accountMatch && accountMatch[1] && accountMatch[1].toLowerCase() !== 'none');
306
+ // 显式未登录特征排查
307
+ const isExplicitlyLoggedOut = /not\s+logged\s+in/i.test(raw) ||
308
+ raw.includes('gh auth login') ||
309
+ raw.includes('authentication required') ||
310
+ raw.includes('No accounts found');
311
+ const loggedIn = !isExplicitlyLoggedOut && (hasLoggedInFlag || hasActiveFlag || (hasTokenFlag && hasAccount));
273
312
  // 只要已登录或有常规输出,则必然安装了 gh
274
313
  const isNotInstalled = Boolean(res.error && (res.error.includes('系统未检测到 GitHub CLI') || res.error.includes('ENOENT')));
275
314
  const installed = loggedIn || !isNotInstalled;
@@ -292,7 +331,6 @@ export class GhExecutor {
292
331
  }
293
332
  // 解析用户名
294
333
  let user;
295
- const accountMatch = raw.match(/account\s+([A-Za-z0-9_-]+)/i);
296
334
  if (accountMatch) {
297
335
  user = accountMatch[1];
298
336
  }
package/lib/main.d.ts CHANGED
@@ -2,6 +2,13 @@ import { GhExecutor } from './executor.js';
2
2
  import './github-service.js';
3
3
  import type { PluginConfig } from './types.js';
4
4
  export declare const name = "dsh-github-flow";
5
+ /**
6
+ * 核心服务依赖声明
7
+ *
8
+ * 必须显式声明 webServer 与 workspaceRegistry:
9
+ * 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
10
+ * ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
11
+ */
5
12
  export declare const inject: readonly ['tools', 'commands', 'webServer', 'workspaceRegistry'];
6
13
  export type Config = PluginConfig;
7
14
  /**
@@ -19,4 +26,9 @@ export declare const Config: {
19
26
  export declare function apply(ctx: any, config?: PluginConfig): void;
20
27
  export type GitHubService = GhExecutor;
21
28
  export * from './types.js';
29
+ export * from './version.js';
30
+ export * from './updater.js';
31
+ export * from './upgrade-manager.js';
32
+ export * from './executor.js';
33
+ export * from './proxy.js';
22
34
  export * from './github-service.js';
package/lib/main.js CHANGED
@@ -4,12 +4,27 @@ 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';
13
+ import { UpdateChecker } from './updater.js';
14
+ import { UpgradeManager } from './upgrade-manager.js';
15
+ import { ProxyManager } from './proxy.js';
16
+ import { NPM_PACKAGE_URL } from './version.js';
9
17
  // 引用即生效:加载本模块以挂上 declare module '@deepseek-ai/cordis' 的 ctx.github 类型声明,
10
18
  // 并随下面 `export * from './github-service.js'` 一起传给消费方。
11
19
  import './github-service.js';
12
20
  export const name = 'dsh-github-flow';
21
+ /**
22
+ * 核心服务依赖声明
23
+ *
24
+ * 必须显式声明 webServer 与 workspaceRegistry:
25
+ * 在 Cordis 依赖隔离机制下,只有在 inject 显式声明的服务,当前插件作用域 ctx 才能持有
26
+ * ctx.webServer 与 ctx.workspaceRegistry 引用,进而注册 Web UI 所需的后端 API 路由。
27
+ */
13
28
  export const inject = ['tools', 'commands', 'webServer', 'workspaceRegistry'];
14
29
  /**
15
30
  * 符合 Standard Schema / Cordis Loader 契约的 Config 运行时校验规范
@@ -28,6 +43,10 @@ export const Config = {
28
43
  maxOutputBytes: typeof cfg.maxOutputBytes === 'number' ? cfg.maxOutputBytes : undefined,
29
44
  cacheTtlMs: typeof cfg.cacheTtlMs === 'number' ? cfg.cacheTtlMs : 15_000,
30
45
  defaultListLimit: typeof cfg.defaultListLimit === 'number' ? cfg.defaultListLimit : 20,
46
+ checkUpdate: typeof cfg.checkUpdate === 'boolean' ? cfg.checkUpdate : true,
47
+ updateCheckIntervalMs: typeof cfg.updateCheckIntervalMs === 'number' ? cfg.updateCheckIntervalMs : 86_400_000,
48
+ registryUrl: typeof cfg.registryUrl === 'string' ? cfg.registryUrl : undefined,
49
+ proxyUrl: typeof cfg.proxyUrl === 'string' ? cfg.proxyUrl : undefined,
31
50
  }
32
51
  };
33
52
  }
@@ -35,6 +54,18 @@ export const Config = {
35
54
  };
36
55
  export function apply(ctx, config = {}) {
37
56
  const executor = new GhExecutor(config);
57
+ // 代理管理器:优先读取用户在驾驶舱保存的 proxy.json,回落 cordis.patch.yml 的
58
+ // proxyUrl 预置值;两者皆无时保持直连(gh 自然继承宿主系统环境变量)。
59
+ // 装配后 executor/upgradeManager/updater 三条外联链路全部经它路由。
60
+ const proxyManager = new ProxyManager(config.proxyUrl);
61
+ executor.attachProxyManager(proxyManager);
62
+ const updateChecker = new UpdateChecker({
63
+ intervalMs: config.updateCheckIntervalMs,
64
+ customRegistry: config.registryUrl,
65
+ });
66
+ updateChecker.applyProxy(proxyManager);
67
+ const upgradeManager = new UpgradeManager();
68
+ upgradeManager.attachProxyManager(proxyManager);
38
69
  // 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
39
70
  if (ctx.provide && typeof ctx.provide === 'function') {
40
71
  ctx.provide('github', executor);
@@ -42,25 +73,63 @@ export function apply(ctx, config = {}) {
42
73
  else {
43
74
  ctx.github = executor;
44
75
  }
45
- // 2. 注册人类 Slash 命令 (/gh status, /gh help)
76
+ // 2. 注册人类 Slash 命令 (/gh status, /gh upgrade, /gh help)
46
77
  if (ctx.commands) {
47
- registerGhCommand(ctx.commands, executor);
78
+ registerGhCommand(ctx.commands, executor, updateChecker);
48
79
  }
49
- // 3. 注册 Agent 模型工具集(5 大核心领域工具)
80
+ // 3. 注册 Agent 模型工具集(核心领域工具 + 元指南)
50
81
  if (ctx.tools) {
51
82
  ctx.tools.register(createPrTool(executor));
52
83
  ctx.tools.register(createIssueTool(executor));
53
84
  ctx.tools.register(createRunTool(executor));
54
85
  ctx.tools.register(createRepoTool(executor));
55
86
  ctx.tools.register(createApiTool(executor));
87
+ ctx.tools.register(createReleaseTool(executor));
88
+ ctx.tools.register(createSecretTool(executor));
89
+ ctx.tools.register(createSkillTool(executor));
90
+ ctx.tools.register(createHelpTool(executor));
56
91
  }
57
92
  // 4. 注册面向 Client 端的 WebServer HTTP 路由(如果当前环境启用了 Web UI 服务)
58
93
  if (ctx.webServer) {
59
94
  // 传入 ctx 作用域对象,使 routes 能够通过 ctx.effect 绑定生命周期并在插件卸载/重载时自动注销;
60
95
  // cacheTtlMs 传给路由层以尊重用户的缓存 TTL 配置。
61
- registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000);
96
+ // proxyManager 传入以挂载 /api/github/proxy* 设置路由(驾驶舱代理设置入口)。
97
+ registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000, updateChecker, upgradeManager, proxyManager);
98
+ }
99
+ // 5. 启动后低优先级后台闲时静默探测:若有新版本,仅输出一次友好的控制台提示
100
+ if (config.checkUpdate !== false) {
101
+ // 包裹在 ctx.effect 中(Cordis 规范):热重载/卸载发生在 3 秒延迟窗口内时,
102
+ // 取消挂起的定时器,避免旧实例在新实例加载后仍向控制台打印升级横幅。
103
+ // 无头/测试环境缺 ctx.effect 时优雅退化为裸定时器(一次性触发,无泄漏)。
104
+ const scheduleProbe = () => {
105
+ const timer = setTimeout(() => {
106
+ updateChecker.check(false).then((info) => {
107
+ if (info.hasUpdate) {
108
+ console.log(`
109
+ ┌────────────────────────────────────────────────────────────────────────┐
110
+ │ 🚀 dsh-github-flow 插件有新版本可用: ${info.currentVersion} -> ${info.latestVersion} │
111
+ │ 升级命令: ${info.upgradeCommand.padEnd(58, ' ')}│
112
+ │ 更新主页: ${NPM_PACKAGE_URL.padEnd(58, ' ')}│
113
+ └────────────────────────────────────────────────────────────────────────┘
114
+ `);
115
+ }
116
+ }).catch(() => { });
117
+ }, 3_000);
118
+ return () => clearTimeout(timer);
119
+ };
120
+ if (typeof ctx.effect === 'function') {
121
+ ctx.effect(scheduleProbe);
122
+ }
123
+ else {
124
+ scheduleProbe();
125
+ }
62
126
  }
63
127
  }
64
128
  export * from './types.js';
129
+ export * from './version.js';
130
+ export * from './updater.js';
131
+ export * from './upgrade-manager.js';
132
+ export * from './executor.js';
133
+ export * from './proxy.js';
65
134
  // 让 `ctx.github` 的类型声明随包一起被消费方加载(声明合并在 './github-service.js' 内)
66
135
  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
+ }