dsh-github-flow 0.1.8 → 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 (60) hide show
  1. package/README.en.md +139 -54
  2. package/README.md +142 -54
  3. package/docs/architecture.md +19 -4
  4. package/docs/images/1.png +0 -0
  5. package/docs/images/3.png +0 -0
  6. package/docs/images/4.png +0 -0
  7. package/docs/npm-publish-guide.md +12 -0
  8. package/lib/api/route-helpers.d.ts +47 -0
  9. package/lib/api/route-helpers.js +134 -0
  10. package/lib/api/routes-actions.d.ts +9 -0
  11. package/lib/api/routes-actions.js +115 -0
  12. package/lib/api/routes-lifecycle.d.ts +9 -0
  13. package/lib/api/routes-lifecycle.js +75 -0
  14. package/lib/api/routes-overview.d.ts +11 -0
  15. package/lib/api/routes-overview.js +227 -0
  16. package/lib/api/routes-proxy.d.ts +18 -0
  17. package/lib/api/routes-proxy.js +63 -0
  18. package/lib/api/routes.d.ts +16 -6
  19. package/lib/api/routes.js +14 -504
  20. package/lib/client.js +383 -55
  21. package/lib/executor.d.ts +17 -0
  22. package/lib/executor.js +28 -0
  23. package/lib/main.d.ts +1 -0
  24. package/lib/main.js +40 -9
  25. package/lib/proxy.d.ts +87 -0
  26. package/lib/proxy.js +275 -0
  27. package/lib/renderers/markdown.d.ts +24 -0
  28. package/lib/renderers/markdown.js +493 -0
  29. package/lib/tools/api.d.ts +4 -0
  30. package/lib/tools/api.js +14 -4
  31. package/lib/tools/help.d.ts +31 -0
  32. package/lib/tools/help.js +212 -0
  33. package/lib/tools/issue.d.ts +32 -1
  34. package/lib/tools/issue.js +94 -17
  35. package/lib/tools/pr.d.ts +22 -1
  36. package/lib/tools/pr.js +67 -15
  37. package/lib/tools/release.d.ts +89 -0
  38. package/lib/tools/release.js +219 -0
  39. package/lib/tools/repo.d.ts +13 -1
  40. package/lib/tools/repo.js +46 -14
  41. package/lib/tools/run.d.ts +22 -1
  42. package/lib/tools/run.js +74 -14
  43. package/lib/tools/secret.d.ts +60 -0
  44. package/lib/tools/secret.js +175 -0
  45. package/lib/tools/skill.d.ts +63 -0
  46. package/lib/tools/skill.js +146 -0
  47. package/lib/types.d.ts +12 -0
  48. package/lib/updater.d.ts +8 -0
  49. package/lib/updater.js +9 -0
  50. package/lib/upgrade-manager.d.ts +4 -0
  51. package/lib/upgrade-manager.js +11 -1
  52. package/package.json +3 -2
  53. package/lib/client/icons.d.ts +0 -13
  54. package/lib/client/icons.js +0 -41
  55. package/lib/client/index.d.ts +0 -3
  56. package/lib/client/index.js +0 -36
  57. package/lib/client/panel.d.ts +0 -1
  58. package/lib/client/panel.js +0 -102
  59. package/lib/client/style.d.ts +0 -1
  60. package/lib/client/style.js +0 -555
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) {
package/lib/main.d.ts CHANGED
@@ -30,4 +30,5 @@ export * from './version.js';
30
30
  export * from './updater.js';
31
31
  export * from './upgrade-manager.js';
32
32
  export * from './executor.js';
33
+ export * from './proxy.js';
33
34
  export * from './github-service.js';
package/lib/main.js CHANGED
@@ -4,10 +4,15 @@ 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';
12
17
  // 引用即生效:加载本模块以挂上 declare module '@deepseek-ai/cordis' 的 ctx.github 类型声明,
13
18
  // 并随下面 `export * from './github-service.js'` 一起传给消费方。
@@ -41,6 +46,7 @@ export const Config = {
41
46
  checkUpdate: typeof cfg.checkUpdate === 'boolean' ? cfg.checkUpdate : true,
42
47
  updateCheckIntervalMs: typeof cfg.updateCheckIntervalMs === 'number' ? cfg.updateCheckIntervalMs : 86_400_000,
43
48
  registryUrl: typeof cfg.registryUrl === 'string' ? cfg.registryUrl : undefined,
49
+ proxyUrl: typeof cfg.proxyUrl === 'string' ? cfg.proxyUrl : undefined,
44
50
  }
45
51
  };
46
52
  }
@@ -48,11 +54,18 @@ export const Config = {
48
54
  };
49
55
  export function apply(ctx, config = {}) {
50
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);
51
62
  const updateChecker = new UpdateChecker({
52
63
  intervalMs: config.updateCheckIntervalMs,
53
64
  customRegistry: config.registryUrl,
54
65
  });
66
+ updateChecker.applyProxy(proxyManager);
55
67
  const upgradeManager = new UpgradeManager();
68
+ upgradeManager.attachProxyManager(proxyManager);
56
69
  // 1. 兑现生态服务契约:将 GitHub 执行引擎作为服务暴露给其他 DSH 插件 (ctx.github)
57
70
  if (ctx.provide && typeof ctx.provide === 'function') {
58
71
  ctx.provide('github', executor);
@@ -64,35 +77,52 @@ export function apply(ctx, config = {}) {
64
77
  if (ctx.commands) {
65
78
  registerGhCommand(ctx.commands, executor, updateChecker);
66
79
  }
67
- // 3. 注册 Agent 模型工具集(5 大核心领域工具)
80
+ // 3. 注册 Agent 模型工具集(核心领域工具 + 元指南)
68
81
  if (ctx.tools) {
69
82
  ctx.tools.register(createPrTool(executor));
70
83
  ctx.tools.register(createIssueTool(executor));
71
84
  ctx.tools.register(createRunTool(executor));
72
85
  ctx.tools.register(createRepoTool(executor));
73
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));
74
91
  }
75
92
  // 4. 注册面向 Client 端的 WebServer HTTP 路由(如果当前环境启用了 Web UI 服务)
76
93
  if (ctx.webServer) {
77
94
  // 传入 ctx 作用域对象,使 routes 能够通过 ctx.effect 绑定生命周期并在插件卸载/重载时自动注销;
78
95
  // cacheTtlMs 传给路由层以尊重用户的缓存 TTL 配置。
79
- registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000, updateChecker, upgradeManager);
96
+ // proxyManager 传入以挂载 /api/github/proxy* 设置路由(驾驶舱代理设置入口)。
97
+ registerApiRoutes(ctx, executor, ctx.workspaceRegistry, config.cacheTtlMs || 15_000, updateChecker, upgradeManager, proxyManager);
80
98
  }
81
99
  // 5. 启动后低优先级后台闲时静默探测:若有新版本,仅输出一次友好的控制台提示
82
100
  if (config.checkUpdate !== false) {
83
- setTimeout(() => {
84
- updateChecker.check(false).then((info) => {
85
- if (info.hasUpdate) {
86
- console.log(`
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(`
87
109
  ┌────────────────────────────────────────────────────────────────────────┐
88
110
  │ 🚀 dsh-github-flow 插件有新版本可用: ${info.currentVersion} -> ${info.latestVersion} │
89
111
  │ 升级命令: ${info.upgradeCommand.padEnd(58, ' ')}│
90
112
  │ 更新主页: ${NPM_PACKAGE_URL.padEnd(58, ' ')}│
91
113
  └────────────────────────────────────────────────────────────────────────┘
92
114
  `);
93
- }
94
- }).catch(() => { });
95
- }, 3_000);
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
+ }
96
126
  }
97
127
  }
98
128
  export * from './types.js';
@@ -100,5 +130,6 @@ export * from './version.js';
100
130
  export * from './updater.js';
101
131
  export * from './upgrade-manager.js';
102
132
  export * from './executor.js';
133
+ export * from './proxy.js';
103
134
  // 让 `ctx.github` 的类型声明随包一起被消费方加载(声明合并在 './github-service.js' 内)
104
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
+ }
package/lib/proxy.js ADDED
@@ -0,0 +1,275 @@
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
+ import fs from 'node:fs';
15
+ import os from 'node:os';
16
+ import path from 'node:path';
17
+ import { createRequire } from 'node:module';
18
+ const DEFAULT_CONFIG = {
19
+ url: '',
20
+ noProxy: 'localhost,127.0.0.1',
21
+ applyToPackageManager: true,
22
+ };
23
+ /** 持久化路径:~/.dsh/data/dsh-github-flow/proxy.json(与 DSH 用户数据目录规范对齐) */
24
+ function resolveStorePath() {
25
+ return path.join(os.homedir(), '.dsh', 'data', 'dsh-github-flow', 'proxy.json');
26
+ }
27
+ /**
28
+ * 校验用户输入的代理地址。
29
+ * 为什么白名单 scheme:地址将注入到全部 gh/npm 子进程环境变量并可能驱动 fetch dispatcher,
30
+ * 放行 file://、gopher:// 等怪异 scheme 属于扩大攻击面;内嵌凭据会随进程 env 暴露,禁止保存。
31
+ */
32
+ export function validateProxyUrl(input) {
33
+ if (typeof input !== 'string' || !input.trim())
34
+ return { ok: false, error: '代理地址不能为空' };
35
+ // 控制字符预处理:WHATWG URL 会静默剥离 NUL 等控制符后继续解析(实测 '\x00http://x'
36
+ // 被接受且 hostname='x'),但注入到子进程 env 的必须是原始串——剥离后 env 与
37
+ // 校验结果不一致,且控制符在环境变量里属滥用输入。直接前置拒绝最干净。
38
+ if (/[\x00-\x1f\x7f]/.test(input))
39
+ return { ok: false, error: '代理地址包含非法控制字符' };
40
+ const raw = input.trim();
41
+ let parsed;
42
+ try {
43
+ // WHATWG URL 原生识别 socks5/socks5h scheme(实测),无需改写即可完成结构校验;
44
+ // 返回给调用方的始终保持用户原始串——注入 env 的值必须与用户输入一致。
45
+ parsed = new URL(raw);
46
+ }
47
+ catch {
48
+ return { ok: false, error: '代理地址格式无效,应为 http://host:port 或 socks5://host:port' };
49
+ }
50
+ // socks5h:// 与 socks5:// 的本质差异在 DNS 归属:socks5 在本机解析(系统 DNS 可能被污染),
51
+ // socks5h 把域名原样交给代理端解析(抗污染更强)。两者均为合法代理形态,一并放行。
52
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:' && parsed.protocol !== 'socks5:' && parsed.protocol !== 'socks5h:') {
53
+ return { ok: false, error: `不支持的协议 ${parsed.protocol},仅允许 http / https / socks5 / socks5h` };
54
+ }
55
+ if (parsed.username || parsed.password) {
56
+ return { ok: false, error: '暂不支持内嵌凭据的代理地址(凭据会随子进程环境变量暴露)' };
57
+ }
58
+ // WHATWG URL 对 "http://" 与 "socks5://" 这类空 host 会静默填空串 hostname,
59
+ // 必须显式拦截;超长主机名同样拒绝(正常代理地址不可能 4KB+,属滥用输入)。
60
+ if (!parsed.hostname)
61
+ return { ok: false, error: '代理地址缺少主机名' };
62
+ if (parsed.hostname.length > 253)
63
+ return { ok: false, error: '代理主机名异常超长' };
64
+ return { ok: true, url: raw };
65
+ }
66
+ /** 归一化 NO_PROXY:强制 localhost,127.0.0.1 置首(本机 Web API 回环请求绝不能被代理劫持) */
67
+ function normalizeNoProxy(input) {
68
+ const raw = typeof input === 'string' ? input : '';
69
+ const parts = raw.split(',').map((s) => s.trim()).filter(Boolean);
70
+ for (const must of ['localhost', '127.0.0.1']) {
71
+ if (!parts.some((p) => p.toLowerCase() === must))
72
+ parts.unshift(must);
73
+ }
74
+ return parts.join(',');
75
+ }
76
+ /**
77
+ * 三级降级解析 undici(仅 fetch dispatcher 需要;gh/npm 子进程链路完全无关)。
78
+ * 1. DSH 宿主运行时 require 链(desktop profile 实测可用);
79
+ * 2. 未来 Node 开放 node:undici 内置模块(22.18 仍为 ERR_UNKNOWN_BUILTIN_MODULE);
80
+ * 3. 均失败 → 返回 null,npm 探测链路保持直连,不炸任何主流程。
81
+ */
82
+ function resolveUndici() {
83
+ try {
84
+ const cordisPkg = path.join(os.homedir(), '.dsh', 'profiles', 'desktop', 'node_modules', '@deepseek-ai', 'cordis', 'package.json');
85
+ const req = createRequire(cordisPkg);
86
+ const u = req('undici');
87
+ if (u && typeof u.ProxyAgent === 'function' && typeof u.setGlobalDispatcher === 'function' && typeof u.getGlobalDispatcher === 'function')
88
+ return u;
89
+ }
90
+ catch { /* 宿主路径不存在(web/CLI profile)→ 走下一级 */ }
91
+ return null;
92
+ }
93
+ export class ProxyManager {
94
+ config = { ...DEFAULT_CONFIG };
95
+ lastProbe;
96
+ fetchProxyAvailable = false;
97
+ activeDispatcherUrl = null;
98
+ /** 安装代理 dispatcher 前的进程默认 dispatcher 快照(关闭代理时恢复,防残留) */
99
+ defaultDispatcher = null;
100
+ constructor(presetUrl) {
101
+ this.load(presetUrl);
102
+ this.applyFetchDispatcher();
103
+ }
104
+ /** 读取持久化文件;不存在时回落 cordis.patch.yml 预置值;再无则保持默认直连 */
105
+ load(presetUrl) {
106
+ try {
107
+ const storePath = resolveStorePath();
108
+ if (fs.existsSync(storePath)) {
109
+ const raw = JSON.parse(fs.readFileSync(storePath, 'utf8'));
110
+ if (raw && typeof raw === 'object') {
111
+ const cfg = typeof raw.config === 'object' && raw.config ? raw.config : raw;
112
+ const url = typeof cfg.url === 'string' ? cfg.url.trim() : '';
113
+ // 文件中的 url 仍走校验:文件可能被手改出非法值,持久化不能成为绕过校验的后门
114
+ this.config = {
115
+ url: validateProxyUrl(url).ok ? url : '',
116
+ noProxy: normalizeNoProxy(cfg.noProxy),
117
+ applyToPackageManager: cfg.applyToPackageManager !== false,
118
+ };
119
+ if (typeof raw.lastProbe === 'object' && raw.lastProbe)
120
+ this.lastProbe = raw.lastProbe;
121
+ return;
122
+ }
123
+ }
124
+ }
125
+ catch { /* 文件损坏/不可读 → 视为未配置,不阻断启动 */ }
126
+ // 无持久化文件:使用 Cordis Config 预置(若管理员在 cordis.patch.yml 配置且合法)
127
+ if (presetUrl && validateProxyUrl(presetUrl).ok) {
128
+ this.config = { ...DEFAULT_CONFIG, url: presetUrl.trim() };
129
+ }
130
+ }
131
+ /**
132
+ * 原子写持久化文件:先写临时文件再 rename,杜绝并发/崩溃产生半截 JSON。
133
+ * 写盘失败(目录权限/沙箱)不抛出:代理是「尽力持久化」语义——内存态已生效,
134
+ * 仅丢失重启恢复能力;用一次 console.warn 提示用户,UI 仍能正常工作。
135
+ */
136
+ persist() {
137
+ try {
138
+ const storePath = resolveStorePath();
139
+ const dir = path.dirname(storePath);
140
+ fs.mkdirSync(dir, { recursive: true });
141
+ const tmp = `${storePath}.${process.pid}.tmp`;
142
+ fs.writeFileSync(tmp, JSON.stringify({ config: this.config, lastProbe: this.lastProbe }, null, 2), 'utf8');
143
+ fs.renameSync(tmp, storePath);
144
+ }
145
+ catch (e) {
146
+ console.warn(`[dsh-github-flow] 代理配置写盘失败(本次会话仍生效,重启后需重新设置): ${e?.message || e}`);
147
+ }
148
+ }
149
+ /**
150
+ * 生成注入子进程的代理环境变量。
151
+ * 未启用时返回 {}——绝不覆盖 process.env 中既有的系统级代理设置(用户不设插件代理
152
+ * 时 gh 自然继承宿主环境变量,即「默认走系统设置」语义)。
153
+ * socks5 对 gh(Go runtime)原生有效;对 npm 同样有效(pacote/agent-base 支持)。
154
+ */
155
+ getEnv(scope = 'gh') {
156
+ if (!this.config.url)
157
+ return {};
158
+ if (scope === 'package-manager' && !this.config.applyToPackageManager)
159
+ return {};
160
+ return {
161
+ HTTPS_PROXY: this.config.url,
162
+ HTTP_PROXY: this.config.url,
163
+ https_proxy: this.config.url,
164
+ http_proxy: this.config.url,
165
+ NO_PROXY: this.config.noProxy,
166
+ no_proxy: this.config.noProxy,
167
+ };
168
+ }
169
+ /**
170
+ * 应用 fetch dispatcher(npm registry 探测链路)。
171
+ * undici 的 ProxyAgent 不支持 socks5:该场景显式标记不可用并保持直连,
172
+ * UI 需如实提示差异(GitHub 链路正常走 socks5),不做静默降级。
173
+ * dispatcher 按 url 缓存重建,url 未变化时零开销。
174
+ *
175
+ * 关闭代理时必须恢复默认全局 dispatcher:setGlobalDispatcher 是进程级副作用,
176
+ * 若只清空本实例状态而不回滚 dispatcher,此前安装的 ProxyAgent 会永久残留,
177
+ * 「启用→关闭」后 npm 探测仍走死代理直到进程重启。首次安装前先快照原 dispatcher。
178
+ */
179
+ applyFetchDispatcher() {
180
+ const url = this.config.url;
181
+ if (!url || url.startsWith('socks5://') || url.startsWith('socks5h://')) {
182
+ // 回滚残留的代理 dispatcher(仅在我们曾经安装过时)
183
+ if (this.activeDispatcherUrl !== null) {
184
+ const undici = resolveUndici();
185
+ if (undici && this.defaultDispatcher) {
186
+ try {
187
+ undici.setGlobalDispatcher(this.defaultDispatcher);
188
+ }
189
+ catch { /* 回滚失败保持现状,下次保存重试 */ }
190
+ }
191
+ this.activeDispatcherUrl = null;
192
+ }
193
+ this.fetchProxyAvailable = false;
194
+ return false;
195
+ }
196
+ const undici = resolveUndici();
197
+ if (!undici) {
198
+ this.fetchProxyAvailable = false;
199
+ return false;
200
+ }
201
+ try {
202
+ // url 变更才重建 dispatcher:高频 apply()(如 UI 反复保存)不会泄漏 agent 连接池
203
+ if (this.activeDispatcherUrl !== url) {
204
+ if (!this.defaultDispatcher) {
205
+ this.defaultDispatcher = undici.getGlobalDispatcher();
206
+ }
207
+ undici.setGlobalDispatcher(new undici.ProxyAgent(url));
208
+ this.activeDispatcherUrl = url;
209
+ }
210
+ this.fetchProxyAvailable = true;
211
+ return true;
212
+ }
213
+ catch {
214
+ this.fetchProxyAvailable = false;
215
+ return false;
216
+ }
217
+ }
218
+ /** 保存新配置并立即生效(写文件 + 重应用 dispatcher),返回完整状态快照 */
219
+ save(input) {
220
+ if (input.url !== undefined && input.url !== '') {
221
+ const v = validateProxyUrl(input.url);
222
+ if (!v.ok)
223
+ return { ok: false, error: v.error, state: this.snapshot() };
224
+ }
225
+ const nextUrl = input.url !== undefined ? (typeof input.url === 'string' ? input.url.trim() : '') : this.config.url;
226
+ this.config = {
227
+ url: nextUrl,
228
+ noProxy: normalizeNoProxy(input.noProxy !== undefined ? input.noProxy : this.config.noProxy),
229
+ applyToPackageManager: typeof input.applyToPackageManager === 'boolean' ? input.applyToPackageManager : this.config.applyToPackageManager,
230
+ };
231
+ this.applyFetchDispatcher();
232
+ this.lastProbe = undefined; // 配置变更后旧探测结果失效
233
+ this.persist();
234
+ return { ok: true, state: this.snapshot() };
235
+ }
236
+ /** 经当前代理配置探测 GitHub API 连通性(2.5s 超时)。失败不回滚保存——离线预配置是合法场景 */
237
+ async probe() {
238
+ const started = Date.now();
239
+ if (!this.config.url) {
240
+ this.lastProbe = { ok: false, at: started, error: '未配置代理地址(当前为系统默认直连)' };
241
+ return this.lastProbe;
242
+ }
243
+ const controller = new AbortController();
244
+ const timer = setTimeout(() => controller.abort(), 2_500);
245
+ try {
246
+ const res = await fetch('https://api.github.com/rate_limit', {
247
+ signal: controller.signal,
248
+ headers: { 'User-Agent': 'dsh-github-flow-proxy-probe' },
249
+ });
250
+ // 探测走的是 Node fetch:只有 http/https 代理且 undici 可用时才真经代理。
251
+ // socks5 场景 fetch 仍直连——探测结果仅代表「GitHub 可达性」,代理有效性以 gh 链路为准。
252
+ this.lastProbe = res.ok
253
+ ? { ok: true, at: started, latencyMs: Date.now() - started }
254
+ : { ok: false, at: started, error: `GitHub API 响应 ${res.status}` };
255
+ }
256
+ catch (e) {
257
+ this.lastProbe = { ok: false, at: started, error: String(e?.cause?.message || e?.message || e).slice(0, 200) };
258
+ }
259
+ finally {
260
+ clearTimeout(timer);
261
+ try {
262
+ this.persist();
263
+ }
264
+ catch { /* 探测结果写盘失败不影响内存态 */ }
265
+ }
266
+ return this.lastProbe;
267
+ }
268
+ snapshot() {
269
+ return {
270
+ config: { ...this.config, noProxy: this.config.noProxy },
271
+ fetchProxyAvailable: this.fetchProxyAvailable,
272
+ lastProbe: this.lastProbe ? { ...this.lastProbe } : undefined,
273
+ };
274
+ }
275
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * DSH GitHub Flow - Native Markdown 格式化渲染引擎
3
+ *
4
+ * 核心目标:
5
+ * 1. 终结机械多行 JSON 输出,为列表和详情产出高密度 Markdown 表格与卡片;
6
+ * 2. 节省 70%+ 的会话上下文 Token,极大加速长对话推理速度;
7
+ * 3. 嵌入可点击超链接与直观的状态药丸 Emoji,提升人类与模型阅读体验;
8
+ * 4. 严谨防御 Markdown 表格竖线破坏、缺失值回退与异常对象降级。
9
+ */
10
+ /** 安全清洗表格单元格文本:转义竖线,压缩换行,限制最大长度 */
11
+ export declare function sanitizeCell(text: any, maxLen?: number): string;
12
+ /** 格式化 PR / Issue 状态药丸徽章 */
13
+ export declare function formatStateBadge(state?: string, isDraft?: boolean): string;
14
+ /** 格式化 CI 工作流状态徽章 */
15
+ export declare function formatCiBadge(status?: string, conclusion?: string): string;
16
+ /** 格式化合并冲突状态 */
17
+ export declare function formatConflictBadge(status?: string): string;
18
+ export declare function renderPrMarkdown(args: any, value: any): string;
19
+ export declare function renderIssueMarkdown(args: any, value: any): string;
20
+ export declare function renderRunMarkdown(args: any, value: any): string;
21
+ export declare function renderRepoMarkdown(args: any, value: any): string;
22
+ export declare function formatReleaseBadge(isDraft?: boolean, isPrerelease?: boolean): string;
23
+ export declare function renderReleaseMarkdown(args: any, value: any): string;
24
+ export declare function renderSkillMarkdown(args: any, value: any): string;