dsh-github-flow 0.4.0 → 0.6.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 (82) hide show
  1. package/README.en.md +91 -12
  2. package/README.md +91 -12
  3. package/docs/UPGRADE-NOTES.md +9 -12
  4. package/docs/architecture.md +63 -29
  5. package/docs/images/1.png +0 -0
  6. package/lib/api/routes-actions.js +268 -5
  7. package/lib/api/routes-overview.js +147 -29
  8. package/lib/cache.d.ts +77 -4
  9. package/lib/cache.js +114 -7
  10. package/lib/client.d.ts +9 -4
  11. package/lib/client.js +5293 -942
  12. package/lib/commands/gh.d.ts +6 -1
  13. package/lib/commands/gh.js +144 -71
  14. package/lib/executor/error-classifier.d.ts +55 -0
  15. package/lib/executor/error-classifier.js +199 -0
  16. package/lib/executor/git-parser.d.ts +157 -1
  17. package/lib/executor/git-parser.js +513 -4
  18. package/lib/executor.d.ts +147 -4
  19. package/lib/executor.js +866 -77
  20. package/lib/main.js +28 -4
  21. package/lib/renderers/agent-task.d.ts +16 -0
  22. package/lib/renderers/agent-task.js +107 -0
  23. package/lib/renderers/codespace.d.ts +16 -0
  24. package/lib/renderers/codespace.js +103 -0
  25. package/lib/renderers/discussion.d.ts +16 -0
  26. package/lib/renderers/discussion.js +101 -0
  27. package/lib/renderers/markdown.d.ts +4 -0
  28. package/lib/renderers/markdown.js +4 -0
  29. package/lib/renderers/project.d.ts +20 -0
  30. package/lib/renderers/project.js +128 -0
  31. package/lib/renderers/skill.d.ts +30 -0
  32. package/lib/renderers/skill.js +180 -0
  33. package/lib/renderers/status.d.ts +5 -0
  34. package/lib/renderers/status.js +52 -0
  35. package/lib/telemetry.d.ts +70 -0
  36. package/lib/telemetry.js +161 -0
  37. package/lib/tools/agent-task.d.ts +54 -0
  38. package/lib/tools/agent-task.js +135 -0
  39. package/lib/tools/codespace.d.ts +51 -0
  40. package/lib/tools/codespace.js +154 -0
  41. package/lib/tools/discussion.d.ts +70 -0
  42. package/lib/tools/discussion.js +195 -0
  43. package/lib/tools/help.js +129 -14
  44. package/lib/tools/image-utils.d.ts +27 -0
  45. package/lib/tools/image-utils.js +73 -3
  46. package/lib/tools/project.d.ts +80 -0
  47. package/lib/tools/project.js +241 -0
  48. package/lib/tools/repo.js +13 -1
  49. package/lib/tools/skill.d.ts +24 -1
  50. package/lib/tools/skill.js +115 -5
  51. package/lib/tools/status.d.ts +3 -2
  52. package/lib/tools/status.js +9 -4
  53. package/lib/types.d.ts +336 -0
  54. package/lib/ui/commit-modal.js +144 -14
  55. package/lib/ui/help-modal.d.ts +21 -0
  56. package/lib/ui/help-modal.js +300 -0
  57. package/lib/ui/issue-modal.d.ts +14 -0
  58. package/lib/ui/issue-modal.js +366 -0
  59. package/lib/ui/markdown-lite.d.ts +66 -0
  60. package/lib/ui/markdown-lite.js +312 -0
  61. package/lib/ui/network-banner.d.ts +12 -0
  62. package/lib/ui/network-banner.js +35 -0
  63. package/lib/ui/pr-modal.d.ts +17 -0
  64. package/lib/ui/pr-modal.js +516 -0
  65. package/lib/ui/repo-badges.d.ts +43 -0
  66. package/lib/ui/repo-badges.js +82 -0
  67. package/lib/ui/repo-selector.d.ts +14 -0
  68. package/lib/ui/repo-selector.js +112 -0
  69. package/lib/ui/storage.d.ts +56 -0
  70. package/lib/ui/storage.js +163 -0
  71. package/lib/ui/styles.d.ts +1 -1
  72. package/lib/ui/styles.js +1219 -0
  73. package/lib/ui/sync-settings-modal.js +1 -1
  74. package/lib/ui/upgrade-card.js +39 -14
  75. package/lib/ui/utils.d.ts +35 -0
  76. package/lib/ui/utils.js +172 -0
  77. package/lib/ui/workspace-panel.js +198 -31
  78. package/lib/upgrade-manager.d.ts +5 -1
  79. package/lib/upgrade-manager.js +24 -1
  80. package/lib/version.d.ts +1 -1
  81. package/lib/version.js +1 -1
  82. package/package.json +2 -2
@@ -1,3 +1,8 @@
1
1
  import type { GhExecutor } from '../executor.js';
2
2
  import type { UpdateChecker } from '../updater.js';
3
- export declare function registerGhCommand(commandsService: any, executor: GhExecutor, updateChecker?: UpdateChecker): void;
3
+ import { TelemetryCollector } from '../telemetry.js';
4
+ /**
5
+ * 注册人类 Slash 快捷指令集
6
+ * 采用扁平化一级指令 + 二级参数容错双轨机制,消除用户“敲了 /gh 不会加参数”的交互盲区
7
+ */
8
+ export declare function registerGhCommand(commandsService: any, executor: GhExecutor, updateChecker?: UpdateChecker, telemetryCollector?: TelemetryCollector): void;
@@ -1,95 +1,168 @@
1
1
  import { CURRENT_PLUGIN_VERSION, PLUGIN_PACKAGE_NAME, NPM_PACKAGE_URL } from '../version.js';
2
2
  import { loadUpdateSettings, describeUpdateMode } from '../update-settings.js';
3
- export function registerGhCommand(commandsService, executor, updateChecker) {
3
+ import { defaultTelemetryCollector } from '../telemetry.js';
4
+ /**
5
+ * 构建 GitHub 状态大盘与诊断报告 Markdown
6
+ */
7
+ async function buildStatusReport(executor, updateChecker, telemetryCollector) {
8
+ // 人类显式诊断必须绕过缓存:用户敲 /gh 往往正是为了确认
9
+ // 「我刚 gh auth login 到底成没成」,给他 30 秒前的缓存等于答非所问。
10
+ const auth = await executor.checkAuth(undefined, true);
11
+ const repo = await executor.getRepoMetadata();
12
+ const updateInfo = updateChecker?.getQuickStatus();
13
+ let md = `### GitHub 集成状态\n\n`;
14
+ // 若检测到新版本,在最上方展示醒目的升级指引横幅
15
+ if (updateInfo?.hasUpdate) {
16
+ md += `> 🚀 **发现新版本 v${updateInfo.latestVersion}**(当前为 \`v${updateInfo.currentVersion}\`)\n`;
17
+ md += `> 输入 \`/gh-upgrade\` 查看升级指引,或在终端执行:\`${updateInfo.upgradeCommand}\`\n\n`;
18
+ }
19
+ md += `- **插件版本**: \`v${updateInfo?.currentVersion || CURRENT_PLUGIN_VERSION}\`\n`;
20
+ if (auth.loggedIn) {
21
+ md += `- **登录账号**: \`${auth.user || '已登录'}\`\n`;
22
+ md += `- **授权作用域**: ${auth.scopes?.map((s) => `\`${s}\``).join(', ') || '标准权限'}\n`;
23
+ }
24
+ else {
25
+ md += `- **登录状态**: ❌ 未登录 GitHub\n`;
26
+ md += `> 提示:请在系统终端执行 \`gh auth login\` 进行登录。\n`;
27
+ }
28
+ if (repo) {
29
+ md += `\n**当前关联仓库**:\n`;
30
+ md += `- 仓库名: [${repo.nameWithOwner}](${repo.url})\n`;
31
+ md += `- 默认分支: \`${repo.defaultBranch}\`\n`;
32
+ if (repo.description)
33
+ md += `- 简介: ${repo.description}\n`;
34
+ }
35
+ else {
36
+ md += `\n*当前目录尚未关联 Git 远程仓库或未托管在 GitHub。*\n`;
37
+ }
38
+ // 遥测运行时健康度与降级率小节
39
+ const collector = telemetryCollector || defaultTelemetryCollector;
40
+ const summary = collector.getSummary();
41
+ let healthBadge = '🟢 极佳';
42
+ if (summary.statusLevel === 'NEEDS_ATTENTION')
43
+ healthBadge = '🔴 需关注';
44
+ else if (summary.statusLevel === 'GOOD')
45
+ healthBadge = '🟡 良好';
46
+ md += `\n**工具链运行时可观测性 (Telemetry)**:\n`;
47
+ md += `- 综合健康度: ${healthBadge} (${summary.healthScore} 分)\n`;
48
+ md += `- 统计样本量: ${summary.totalCalls} 次 Agent 工具调用\n`;
49
+ md += `- API 降级率: \`${summary.degradationRate}%\` (${summary.degradedCount} 次降级)\n`;
50
+ if (summary.recommendations.length > 0 && summary.degradationRate > 0) {
51
+ md += `> 💡 优化建议: ${summary.recommendations[0]}\n`;
52
+ }
53
+ md += `\n> 💡 **快捷指令指引**: 可直接输入 \`/gh-upgrade\` 检查组件更新,输入 \`/gh-help\` 查看 Agent 协作范例。`;
54
+ return md;
55
+ }
56
+ /**
57
+ * 构建插件版本升级指南 Markdown
58
+ */
59
+ async function buildUpgradeReport(updateChecker) {
60
+ const info = updateChecker ? await updateChecker.check(true) : null;
61
+ const curVer = info?.currentVersion || CURRENT_PLUGIN_VERSION;
62
+ const latestVer = info?.latestVersion || curVer;
63
+ const hasUpdate = info?.hasUpdate ?? false;
64
+ const isMac = process.platform === 'darwin';
65
+ const isWin = process.platform === 'win32';
66
+ const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
67
+ let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
68
+ md += `- **当前运行版本**: \`v${curVer}\`\n`;
69
+ // 当前更新源:排查「查不到新版」时用户需可见自己在用哪个源
70
+ const liveMode = updateChecker?.getSourceMode?.() ?? null;
71
+ const effectiveMode = liveMode ?? loadUpdateSettings();
72
+ md += `- **当前更新源**: ${describeUpdateMode(effectiveMode)}\n`;
73
+ md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
74
+ md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
75
+ md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
76
+ md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
77
+ md += `1. **DSH 桌面版 Profile 环境升级(推荐)**:\n\`\`\`bash\ndsh plugin --profile desktop add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
78
+ md += `2. **DSH Web / CLI 环境**(profile 名按实际环境填写):\n\`\`\`bash\ndsh plugin --profile web add ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n`;
79
+ if (isMac) {
80
+ md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
81
+ }
82
+ else {
83
+ md += `\n`;
84
+ }
85
+ md += `3. **Node 全局环境升级**:\n\`\`\`bash\nnpm install -g ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
86
+ md += `> 💡 **升级后提示**:Node.js 运行时对已载入模块存在强缓存,升级后**请重启 DSH 服务以完全加载最新后端工具**。`;
87
+ return md;
88
+ }
89
+ /**
90
+ * 构建使用帮助与 Agent 提示词范例 Markdown
91
+ */
92
+ function buildHelpReport() {
93
+ return [
94
+ '### 🧭 DSH GitHub Flow 快捷指令帮助\n',
95
+ '- `/gh`:查看当前 GitHub CLI 账号认证状态、关联仓库与运行时健康度(直接回车即可)',
96
+ '- `/gh-upgrade`:检查新版本并输出针对当前系统的最佳升级命令',
97
+ '- `/gh-help`:显示此使用指南与 Agent 对话范例\n',
98
+ '*(提示:熟练用户亦可使用二级语法,如 `/gh upgrade`、`/gh status`)*\n',
99
+ '#### 可以在对话框直接让 Agent 操作 GitHub,例如:',
100
+ '- *"帮我看看今天在 GitHub 上有哪些需要我处理的事情?"*',
101
+ '- *"查看当前仓库有哪些开放的 PR,审查 PR #12 的代码差异"*',
102
+ '- *"查看最近一次 CI 失败的原因并尝试自动修复"*',
103
+ '- *"帮我给当前分支创建一个 PR 并附带改动说明"*',
104
+ '- *"在当前仓库发起一个新的 RFC 讨论方案"*',
105
+ ].join('\n');
106
+ }
107
+ /**
108
+ * 注册人类 Slash 快捷指令集
109
+ * 采用扁平化一级指令 + 二级参数容错双轨机制,消除用户“敲了 /gh 不会加参数”的交互盲区
110
+ */
111
+ export function registerGhCommand(commandsService, executor, updateChecker, telemetryCollector) {
4
112
  if (!commandsService || typeof commandsService.register !== 'function')
5
113
  return;
114
+ // 1. 主指令 /gh(默认直接回车输出状态,同时兼容 /gh status, /gh upgrade, /gh help 及常见别名)
6
115
  commandsService.register({
7
116
  name: 'gh',
8
- description: 'GitHub CLI 状态与诊断快捷指令',
117
+ description: '查看 GitHub 账号状态、关联仓库与运行时健康度(直接回车即可执行)',
9
118
  async handler({ rawInput }) {
10
- const trimmed = (rawInput || '').trim();
11
- if (!trimmed || trimmed === 'status') {
12
- // 人类显式诊断必须绕过缓存:用户敲 /gh status 往往正是为了确认
13
- // 「我刚 gh auth login 到底成没成」,给他 30 秒前的缓存等于答非所问。
14
- const auth = await executor.checkAuth(undefined, true);
15
- const repo = await executor.getRepoMetadata();
16
- const updateInfo = updateChecker?.getQuickStatus();
17
- let md = `### GitHub 集成状态\n\n`;
18
- // 若检测到新版本,在最上方展示醒目的升级指引横幅
19
- if (updateInfo?.hasUpdate) {
20
- md += `> 🚀 **发现新版本 v${updateInfo.latestVersion}**(当前为 \`v${updateInfo.currentVersion}\`)\n`;
21
- md += `> 输入 \`/gh upgrade\` 查看升级指引,或在终端执行:\`${updateInfo.upgradeCommand}\`\n\n`;
22
- }
23
- md += `- **插件版本**: \`v${updateInfo?.currentVersion || CURRENT_PLUGIN_VERSION}\`\n`;
24
- if (auth.loggedIn) {
25
- md += `- **登录账号**: \`${auth.user || '已登录'}\`\n`;
26
- md += `- **授权作用域**: ${auth.scopes?.map((s) => `\`${s}\``).join(', ') || '标准权限'}\n`;
27
- }
28
- else {
29
- md += `- **登录状态**: ❌ 未登录 GitHub\n`;
30
- md += `> 提示:请在系统终端执行 \`gh auth login\` 进行登录。\n`;
31
- }
32
- if (repo) {
33
- md += `\n**当前关联仓库**:\n`;
34
- md += `- 仓库名: [${repo.nameWithOwner}](${repo.url})\n`;
35
- md += `- 默认分支: \`${repo.defaultBranch}\`\n`;
36
- if (repo.description)
37
- md += `- 简介: ${repo.description}\n`;
38
- }
39
- else {
40
- md += `\n*当前目录尚未关联 Git 远程仓库或未托管在 GitHub。*\n`;
41
- }
119
+ const trimmed = (rawInput || '').trim().toLowerCase();
120
+ // 智能容错:无参数、status、info、auth 均直接直出状态大盘
121
+ if (!trimmed || trimmed === 'status' || trimmed === 'info' || trimmed === 'auth' || trimmed === 'check') {
42
122
  return {
43
123
  kind: 'success',
44
- text: md,
124
+ text: await buildStatusReport(executor, updateChecker, telemetryCollector),
45
125
  };
46
126
  }
47
- if (trimmed === 'upgrade') {
48
- const info = updateChecker ? await updateChecker.check(true) : null;
49
- const curVer = info?.currentVersion || CURRENT_PLUGIN_VERSION;
50
- const latestVer = info?.latestVersion || curVer;
51
- const hasUpdate = info?.hasUpdate ?? false;
52
- const isMac = process.platform === 'darwin';
53
- const isWin = process.platform === 'win32';
54
- const platformName = isMac ? 'macOS' : isWin ? 'Windows' : 'Linux';
55
- let md = `### 🚀 ${PLUGIN_PACKAGE_NAME} 插件升级指南\n\n`;
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`;
63
- md += `- **官方最新版本**: \`v${latestVer}\` ${hasUpdate ? '🔥 (有可用更新)' : '✅ (已是最新)'}\n`;
64
- md += `- **操作系统环境**: \`${platformName} (${process.platform})\`\n`;
65
- md += `- **npm 仓库地址**: [${NPM_PACKAGE_URL}](${NPM_PACKAGE_URL})\n\n`;
66
- md += `#### 请根据你的 DSH 安装环境选择适用的升级方式:\n\n`;
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`;
71
- if (isMac) {
72
- md += `> 💡 *macOS 提示*:若在终端找不到 \`dsh\` 命令,可直接在桌面端目录更新:\n\`\`\`bash\ncd ~/.dsh/profiles/desktop && npm install ${PLUGIN_PACKAGE_NAME}@latest --save\n\`\`\`\n\n`;
73
- }
74
- else {
75
- md += `\n`;
76
- }
77
- md += `3. **Node 全局环境升级**:\n\`\`\`bash\nnpm install -g ${PLUGIN_PACKAGE_NAME}@latest\n\`\`\`\n\n`;
78
- md += `> 💡 **升级后提示**:Node.js 运行时对已载入模块存在强缓存,升级后**请重启 DSH 服务以完全加载最新后端工具**。`;
127
+ // 二级 upgrade / update 别名容错
128
+ if (trimmed === 'upgrade' || trimmed === 'update' || trimmed === 'up' || trimmed === 'version') {
79
129
  return {
80
130
  kind: 'success',
81
- text: md,
131
+ text: await buildUpgradeReport(updateChecker),
82
132
  };
83
133
  }
84
- if (trimmed === 'help') {
134
+ // 二级 help / ? 别名容错
135
+ if (trimmed === 'help' || trimmed === 'h' || trimmed === '?') {
85
136
  return {
86
137
  kind: 'success',
87
- 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"*`,
138
+ text: buildHelpReport(),
88
139
  };
89
140
  }
90
141
  return {
91
142
  kind: 'error',
92
- text: `未知命令选项: /gh ${trimmed}。可用用法: /gh status, /gh upgrade, /gh help`,
143
+ text: `未知命令选项: /gh ${rawInput}。可用快捷指令: /gh(查看状态)、/gh-upgrade(检查升级)、/gh-help(帮助)。`,
144
+ };
145
+ },
146
+ });
147
+ // 2. 扁平化一级指令 /gh-upgrade(下拉菜单一键可见,无需手动输入空格与子命令)
148
+ commandsService.register({
149
+ name: 'gh-upgrade',
150
+ description: '检查插件新版本并查看一键自动升级指引',
151
+ async handler() {
152
+ return {
153
+ kind: 'success',
154
+ text: await buildUpgradeReport(updateChecker),
155
+ };
156
+ },
157
+ });
158
+ // 3. 扁平化一级指令 /gh-help(下拉菜单一键可见,查看指南与 Agent 提示词范例)
159
+ commandsService.register({
160
+ name: 'gh-help',
161
+ description: '查看 GitHub 协作帮助与 Agent 提示词范例',
162
+ async handler() {
163
+ return {
164
+ kind: 'success',
165
+ text: buildHelpReport(),
93
166
  };
94
167
  },
95
168
  });
@@ -0,0 +1,55 @@
1
+ /**
2
+ * gh 错误语义分类(确定性配置错误 vs 可重试的传输层故障)。
3
+ *
4
+ * 为什么必须区分:`gh` 失败时一律以非零退出码返回,调用方只能看到一段 stderr
5
+ * 文本。过去上层(Web 概览路由)把所有非零退出统一当成「网络错误」,于是
6
+ * 「仓库关闭了 Issues」这种永久性配置事实被伪装成网络抖动——既弹出误导性的
7
+ * 「网络连接失败」横幅,又触发 3 轮阶梯退避重试与每 5-10 分钟一轮的自动同步,
8
+ * 把确定性失败当成偶发抖动反复空烧。
9
+ *
10
+ * 判据全部来自 gh 2.96 实测 stderr:
11
+ * - the 'o/r' repository has disabled issues
12
+ * - the 'o/r' repository has discussions disabled
13
+ * - GraphQL: Could not resolve to a Repository with the name 'o/r'. (repository)
14
+ * - HTTP 403 / Not Found (HTTP 404)
15
+ *
16
+ * 纯文本判定,不做任何 I/O——它是 GhResult.errorKind 的唯一事实源。
17
+ */
18
+ /** gh 失败结果的语义种类 */
19
+ export type GhErrorKind =
20
+ /** 传输层故障(超时、DNS、连接重置、代理阻断):值得重试,允许回退历史快照 */
21
+ 'transient'
22
+ /** 确定性失败(仓库关闭 issues、仓库不存在、权限不足):重试永远无意义 */
23
+ | 'deterministic'
24
+ /** 命令失败但无明确归属,按可重试处理(保守不误杀) */
25
+ | 'unknown';
26
+ /**
27
+ * 判定一次 gh 失败的语义种类。
28
+ *
29
+ * 优先级设计:确定性 > 限流放行 > 传输层 > unknown。
30
+ * 为什么不把「同时命中确定性与传输层」判为 transient:若仓库关闭了 Issues,
31
+ * 网络恢复后该命令依然失败。判成 transient 会让调用方每轮回退旧快照、
32
+ * 永远收敛不到正确状态。确定性一旦命中就该立刻停止重试。
33
+ */
34
+ export declare function classifyGhError(errorText: string | undefined | null): GhErrorKind;
35
+ /**
36
+ * 把确定性失败翻译成面向用户的友好说明。
37
+ * 返回 null 表示不属于确定性失败,调用方应走原有网络故障路径。
38
+ */
39
+ export declare function describeDeterministicError(errorText: string | undefined | null): string | null;
40
+ /** 该错误是否值得重试(供 UI 层决定是否走阶梯退避) */
41
+ export declare function isRetryableError(errorText: string | undefined | null): boolean;
42
+ export declare function isPermissionError(errorText: string | undefined | null): boolean;
43
+ /**
44
+ * 把「权限不足」翻译成可操作的处置指引。
45
+ *
46
+ * 为什么不能只回一句「无权限」:Agent 与用户都需要知道**下一步做什么**。
47
+ * GitHub 的权限错误有明确解法路径(提权到 Triage/Write、或改用 fork 工作流),
48
+ * 把这条路径说出来,比让用户自己搜错误文本有用得多。
49
+ *
50
+ * @param errorText gh 的 stderr
51
+ * @param action 可选:正在执行的动作标识(如 'merge' / 'close' / 'add_label'),
52
+ * 用于在错误文本特征不足时精准匹配
53
+ * @returns 指引文本;非权限错误返回 null(不产生噪音)
54
+ */
55
+ export declare function permissionRemedy(errorText: string | undefined | null, action?: string): string | null;
@@ -0,0 +1,199 @@
1
+ /**
2
+ * gh 错误语义分类(确定性配置错误 vs 可重试的传输层故障)。
3
+ *
4
+ * 为什么必须区分:`gh` 失败时一律以非零退出码返回,调用方只能看到一段 stderr
5
+ * 文本。过去上层(Web 概览路由)把所有非零退出统一当成「网络错误」,于是
6
+ * 「仓库关闭了 Issues」这种永久性配置事实被伪装成网络抖动——既弹出误导性的
7
+ * 「网络连接失败」横幅,又触发 3 轮阶梯退避重试与每 5-10 分钟一轮的自动同步,
8
+ * 把确定性失败当成偶发抖动反复空烧。
9
+ *
10
+ * 判据全部来自 gh 2.96 实测 stderr:
11
+ * - the 'o/r' repository has disabled issues
12
+ * - the 'o/r' repository has discussions disabled
13
+ * - GraphQL: Could not resolve to a Repository with the name 'o/r'. (repository)
14
+ * - HTTP 403 / Not Found (HTTP 404)
15
+ *
16
+ * 纯文本判定,不做任何 I/O——它是 GhResult.errorKind 的唯一事实源。
17
+ */
18
+ /**
19
+ * 确定性失败的 stderr 特征表。
20
+ *
21
+ * 为什么用特征串而不是退出码:gh 对上述场景统一返回 exit 1,退出码无法区分
22
+ * 「仓库关了 Issues」与「网线掉了」。文本特征是唯一可用的判据。
23
+ * 每条都必须来自实测输出——误判会把真实网络故障降级成静默空数据,比误报更糟。
24
+ */
25
+ const DETERMINISTIC_PATTERNS = [
26
+ // 仓库级开关关闭(Issues / Discussions / Projects / Wiki)。
27
+ // 注意 gh 的措辞不统一(实测):
28
+ // issues → "repository has disabled issues"
29
+ // discussions → "repository has discussions disabled"
30
+ // 词序相反,所以两种形态都要覆盖,不能只写一种。
31
+ {
32
+ re: /repository has (?:disabled )?(issues|discussions|projects|wiki)(?: disabled)?/i,
33
+ reason: '该仓库未开启该项协作功能',
34
+ },
35
+ // 仓库不可达(不存在 / 无权限 / 已改名):GraphQL 与 REST 两条通道
36
+ { re: /Could not resolve to a Repository/i, reason: '仓库不存在或当前账号无权访问' },
37
+ { re: /Not Found \(HTTP 404\)/i, reason: '仓库或资源不存在' },
38
+ // 分支 / PR / Issue 单体不存在
39
+ { re: /Branch not found/i, reason: '分支不存在' },
40
+ // GraphQL 通道的权限不足(实测形态):
41
+ // GraphQL: ItQianChen does not have the correct permissions to execute `AddLabelsToLabelable` (addLabelsToLabelable)
42
+ // GraphQL: ItQianChen does not have the correct permissions to execute `DeleteIssue` (deleteIssue)
43
+ // 这类文本里没有 "HTTP 403",若不单列会被归为 unknown,进而被当成可重试抖动——
44
+ // 而权限不足重试一万次也不会变。
45
+ { re: /does not have the correct permissions to execute/i, reason: '当前账号对该仓库的权限不足' },
46
+ ];
47
+ /** 把「仓库未开启某项功能」翻译成点名说明(比笼统的“未开启协作功能”有用) */
48
+ const DISABLED_FEATURE_LABEL = {
49
+ issues: 'Issues',
50
+ discussions: 'Discussions',
51
+ projects: 'Projects',
52
+ wiki: 'Wiki',
53
+ };
54
+ /**
55
+ * 限流类 403 必须先于通用 403 放行。
56
+ * 为什么单列:GitHub 的 `HTTP 403` 既可能是「无权限」(确定性),也可能是
57
+ * 「API rate limit exceeded / secondary rate limit」(几分钟后可恢复)。
58
+ * 把限流误判成确定性会让学生等一会儿就能好的请求被判死刑。
59
+ */
60
+ const RATE_LIMIT_RE = /rate limit|secondary rate limit|abuse detection/i;
61
+ /** 传输层故障特征(显式白名单,用于给出「可重试」的准确语义) */
62
+ const TRANSIENT_PATTERNS = [
63
+ /命令执行超时/,
64
+ /\bETIMEDOUT\b/i,
65
+ /\bECONNRESET\b/i,
66
+ /\bECONNREFUSED\b/i,
67
+ /\bENOTFOUND\b/i,
68
+ /\bEAI_AGAIN\b/i,
69
+ /socket hang up/i,
70
+ /TLS handshake/i,
71
+ /proxyconnect/i,
72
+ /spawn EPERM/i,
73
+ /network is unreachable/i,
74
+ /Could not resolve host/i,
75
+ /no such host/i,
76
+ ];
77
+ /**
78
+ * 判定一次 gh 失败的语义种类。
79
+ *
80
+ * 优先级设计:确定性 > 限流放行 > 传输层 > unknown。
81
+ * 为什么不把「同时命中确定性与传输层」判为 transient:若仓库关闭了 Issues,
82
+ * 网络恢复后该命令依然失败。判成 transient 会让调用方每轮回退旧快照、
83
+ * 永远收敛不到正确状态。确定性一旦命中就该立刻停止重试。
84
+ */
85
+ export function classifyGhError(errorText) {
86
+ const text = String(errorText || '');
87
+ if (!text)
88
+ return 'unknown';
89
+ // 限流 403 先放行,避免被下面的通用 403 规则当成确定性失败
90
+ if (RATE_LIMIT_RE.test(text))
91
+ return 'transient';
92
+ for (const { re } of DETERMINISTIC_PATTERNS) {
93
+ if (re.test(text))
94
+ return 'deterministic';
95
+ }
96
+ // 通用 403 放在确定性表之后、transient 之前:无权限属于确定性
97
+ if (/HTTP 403/i.test(text))
98
+ return 'deterministic';
99
+ for (const re of TRANSIENT_PATTERNS) {
100
+ if (re.test(text))
101
+ return 'transient';
102
+ }
103
+ return 'unknown';
104
+ }
105
+ /**
106
+ * 把确定性失败翻译成面向用户的友好说明。
107
+ * 返回 null 表示不属于确定性失败,调用方应走原有网络故障路径。
108
+ */
109
+ export function describeDeterministicError(errorText) {
110
+ const text = String(errorText || '');
111
+ if (!text)
112
+ return null;
113
+ if (RATE_LIMIT_RE.test(text))
114
+ return null;
115
+ // 先处理「仓库关闭某项功能」:点名到具体功能,比笼统措辞更容易让用户行动
116
+ const disabled = /repository has (?:disabled )?(issues|discussions|projects|wiki)(?: disabled)?/i.exec(text);
117
+ if (disabled) {
118
+ const label = DISABLED_FEATURE_LABEL[disabled[1].toLowerCase()] || disabled[1];
119
+ return `该仓库未开启 ${label}`;
120
+ }
121
+ for (const { re, reason } of DETERMINISTIC_PATTERNS) {
122
+ if (re.test(text))
123
+ return reason;
124
+ }
125
+ if (/HTTP 403/i.test(text))
126
+ return '当前账号无此操作权限';
127
+ return null;
128
+ }
129
+ /** 该错误是否值得重试(供 UI 层决定是否走阶梯退避) */
130
+ export function isRetryableError(errorText) {
131
+ return classifyGhError(errorText) !== 'deterministic';
132
+ }
133
+ /**
134
+ * 判断一段错误是否属于「权限不足」(而非仓库配置、不存在等确定性错误)。
135
+ *
136
+ * 为什么单独判定:确定性错误里只有权限这一类有「解法路径」(提权 / 换 fork 工作流),
137
+ * 其余(仓库关了 Issues、分支不存在)说再多也没用。
138
+ */
139
+ const PERMISSION_RE = /does not have the correct permissions to execute|HTTP 403|Resource not accessible|Forbidden|must have (?:admin|push|write|triage|maintain) (?:access|permissions?)/i;
140
+ export function isPermissionError(errorText) {
141
+ const text = String(errorText || '');
142
+ if (!text)
143
+ return false;
144
+ // 限流也常以 403 出现,但它不是权限问题——放行会让用户去改权限,做无用功
145
+ if (RATE_LIMIT_RE.test(text))
146
+ return false;
147
+ return PERMISSION_RE.test(text);
148
+ }
149
+ /**
150
+ * 已知操作 → 所需权限档位 的映射。
151
+ *
152
+ * 键取自 GitHub 的 GraphQL mutation 名与 gh 子命令动词(实测捕获)。
153
+ * 为什么不用完整枚举:GitHub 有上百个 mutation,穷举既不可能也没必要——
154
+ * 未命中的一律走默认指引,仍然是对的。
155
+ */
156
+ const ACTION_PERMISSION_MAP = [
157
+ {
158
+ // 同时覆盖 GraphQL mutation 名(AddLabelsToLabelable)与 CLI 参数形态
159
+ // (--add-label / --remove-label)——两者出现在不同层级的错误文本里。
160
+ re: /AddLabelsToLabelable|RemoveLabelsFromLabelable|UpdateIssue|CloseIssue|TransferIssue|add-?label|remove-?label|close-?issue|issue (?:edit|close|pin|lock|transfer)/i,
161
+ level: 'Triage',
162
+ hint: '管理他人 Issue/PR(加标签、关闭、指派)需要 Triage 及以上权限。可请仓库管理员授予,或改用评论表达意见。',
163
+ },
164
+ {
165
+ re: /mergePullRequest|MergePullRequest|\bmerge\b|CreateRef|createRef|\bpush\b|CreateCommitOnBranch/i,
166
+ level: 'Write',
167
+ hint: '合并 PR / 推送代码需要 Write 及以上权限。若是他人仓库,请先 fork 到自己的账号,在 fork 上提交后向上游发起 PR。',
168
+ },
169
+ {
170
+ re: /DeleteIssue|deleteIssue|DeleteRepository|deleteRepository|TransferRepository|\bdelete\b/i,
171
+ level: 'Admin',
172
+ hint: '物理删除与仓库级操作需要管理员权限。这类操作不可逆,请联系仓库管理员执行。',
173
+ },
174
+ ];
175
+ /**
176
+ * 把「权限不足」翻译成可操作的处置指引。
177
+ *
178
+ * 为什么不能只回一句「无权限」:Agent 与用户都需要知道**下一步做什么**。
179
+ * GitHub 的权限错误有明确解法路径(提权到 Triage/Write、或改用 fork 工作流),
180
+ * 把这条路径说出来,比让用户自己搜错误文本有用得多。
181
+ *
182
+ * @param errorText gh 的 stderr
183
+ * @param action 可选:正在执行的动作标识(如 'merge' / 'close' / 'add_label'),
184
+ * 用于在错误文本特征不足时精准匹配
185
+ * @returns 指引文本;非权限错误返回 null(不产生噪音)
186
+ */
187
+ export function permissionRemedy(errorText, action) {
188
+ if (!isPermissionError(errorText))
189
+ return null;
190
+ const haystack = `${String(errorText || '')} ${String(action || '')}`;
191
+ for (const { re, hint } of ACTION_PERMISSION_MAP) {
192
+ if (re.test(haystack))
193
+ return hint;
194
+ }
195
+ // 默认指引:不指定具体档位。
196
+ // 为什么:只知道「权限不足」时猜档位很容易猜错(我们实测就猜错过——
197
+ // READ 用户能建 Issue,而不是处处受限),说错比不说更糟。
198
+ return '当前账号对该仓库的权限不足。可运行 `gh auth status` 查看作用域,或请仓库管理员提升权限。';
199
+ }