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.
Files changed (69) hide show
  1. package/README.en.md +162 -58
  2. package/README.md +167 -60
  3. package/docs/UPGRADE-NOTES.md +93 -0
  4. package/docs/architecture.md +47 -17
  5. package/docs/images/1.png +0 -0
  6. package/docs/images/2.png +0 -0
  7. package/docs/images/3.png +0 -0
  8. package/docs/images/4.png +0 -0
  9. package/docs/npm-publish-guide.md +26 -3
  10. package/lib/api/route-helpers.d.ts +47 -0
  11. package/lib/api/route-helpers.js +134 -0
  12. package/lib/api/routes-actions.d.ts +23 -0
  13. package/lib/api/routes-actions.js +239 -0
  14. package/lib/api/routes-lifecycle.d.ts +9 -0
  15. package/lib/api/routes-lifecycle.js +126 -0
  16. package/lib/api/routes-overview.d.ts +11 -0
  17. package/lib/api/routes-overview.js +227 -0
  18. package/lib/api/routes-proxy.d.ts +18 -0
  19. package/lib/api/routes-proxy.js +63 -0
  20. package/lib/api/routes.d.ts +16 -6
  21. package/lib/api/routes.js +14 -504
  22. package/lib/client.js +785 -90
  23. package/lib/commands/gh.js +11 -2
  24. package/lib/executor.d.ts +17 -0
  25. package/lib/executor.js +146 -8
  26. package/lib/main.d.ts +1 -0
  27. package/lib/main.js +49 -9
  28. package/lib/proxy.d.ts +87 -0
  29. package/lib/proxy.js +275 -0
  30. package/lib/renderers/markdown.d.ts +29 -0
  31. package/lib/renderers/markdown.js +520 -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 +224 -0
  36. package/lib/tools/image-utils.d.ts +79 -0
  37. package/lib/tools/image-utils.js +247 -0
  38. package/lib/tools/issue.d.ts +40 -4
  39. package/lib/tools/issue.js +167 -19
  40. package/lib/tools/pr.d.ts +29 -4
  41. package/lib/tools/pr.js +144 -18
  42. package/lib/tools/release.d.ts +89 -0
  43. package/lib/tools/release.js +219 -0
  44. package/lib/tools/repo.d.ts +24 -4
  45. package/lib/tools/repo.js +114 -14
  46. package/lib/tools/run.d.ts +22 -1
  47. package/lib/tools/run.js +74 -14
  48. package/lib/tools/secret.d.ts +60 -0
  49. package/lib/tools/secret.js +175 -0
  50. package/lib/tools/skill.d.ts +63 -0
  51. package/lib/tools/skill.js +146 -0
  52. package/lib/types.d.ts +24 -1
  53. package/lib/update-settings.d.ts +18 -0
  54. package/lib/update-settings.js +60 -0
  55. package/lib/updater.d.ts +40 -1
  56. package/lib/updater.js +126 -9
  57. package/lib/upgrade-manager.d.ts +74 -1
  58. package/lib/upgrade-manager.js +356 -46
  59. package/lib/version.d.ts +1 -1
  60. package/lib/version.js +1 -1
  61. package/package.json +3 -2
  62. package/lib/client/icons.d.ts +0 -13
  63. package/lib/client/icons.js +0 -41
  64. package/lib/client/index.d.ts +0 -3
  65. package/lib/client/index.js +0 -36
  66. package/lib/client/panel.d.ts +0 -1
  67. package/lib/client/panel.js +0 -102
  68. package/lib/client/style.d.ts +0 -1
  69. package/lib/client/style.js +0 -555
package/lib/api/routes.js CHANGED
@@ -1,102 +1,14 @@
1
- import path from 'node:path';
2
1
  import { TtlCache } from '../cache.js';
3
- import { mapLimit, tryReadWorkspaces, findWorkspacePathBySession, findMostRecentWorkspacePath, hasGitDir } from '../workspace.js';
4
2
  import { UpdateChecker } from '../updater.js';
5
3
  import { UpgradeManager } from '../upgrade-manager.js';
6
- /**
7
- * 依据当前登录账号判定 Issue 的归属身份维度 (可多选):
8
- * - reported: 用户提起的 (属于当前账号名下仓库,但作者不是本人)
9
- * - assigned: 被分配的 (指派列表中包含当前账号)
10
- * - created: 我创建的 (作者是当前账号)
11
- *
12
- * 为什么这样划分:
13
- * 在真实工程实践中,Maintainer 不仅要关注自己创建的任务,更要对社区/用户提交的 Bug 反馈负责,
14
- * 同时还要处理他人指派的待办任务。这三个维度互补且可能重叠。
15
- */
16
- function resolveIssueCategories(issue, currentUser, explicitRepoOwner) {
17
- const categories = [];
18
- if (!currentUser)
19
- return categories;
20
- const currentLower = currentUser.toLowerCase();
21
- const authorLogin = issue.author?.login?.toLowerCase();
22
- const isAuthor = Boolean(authorLogin && authorLogin === currentLower);
23
- const isAssignee = Array.isArray(issue.assignees) &&
24
- issue.assignees.some((a) => a.login?.toLowerCase() === currentLower);
25
- // 仓库 owner 判定:优先从 repository.nameWithOwner 提取,兜底使用 explicitRepoOwner
26
- let repoOwner = explicitRepoOwner?.toLowerCase();
27
- if (!repoOwner && issue.repository?.nameWithOwner) {
28
- repoOwner = issue.repository.nameWithOwner.split('/')[0]?.toLowerCase();
29
- }
30
- const isOwnerRepo = Boolean(repoOwner && repoOwner === currentLower);
31
- if (isAuthor)
32
- categories.push('created');
33
- if (isAssignee)
34
- categories.push('assigned');
35
- // 如果是当前用户维护的仓库,且作者不是本人,则明确归为「用户提起的」
36
- if (isOwnerRepo && !isAuthor)
37
- categories.push('reported');
38
- return categories;
39
- }
40
- function calculateIssueStats(issues) {
41
- let reported = 0;
42
- let assigned = 0;
43
- let created = 0;
44
- for (const item of issues) {
45
- const cats = item.categories || [];
46
- if (cats.includes('reported'))
47
- reported++;
48
- if (cats.includes('assigned'))
49
- assigned++;
50
- if (cats.includes('created'))
51
- created++;
52
- }
53
- return {
54
- total: issues.length,
55
- reported,
56
- assigned,
57
- created,
58
- };
59
- }
60
- /** path.basename 在空串/异常输入上会抛错,工作区路径来自磁盘文件,必须防御 */
61
- function basenameSafe(p) {
62
- try {
63
- return path.basename(p) || p;
64
- }
65
- catch {
66
- return p;
67
- }
68
- }
69
- /** 探测单个工作区时在途的 gh 进程数上限;libuv 线程池默认 4,留出余量给其他请求 */
70
- const WORKSPACE_PROBE_CONCURRENCY = 3;
71
- /**
72
- * 全局驾驶舱的缓存时长下限。
73
- * 这个端点要 spawn 十几次 gh,比单仓库概览重得多,因此不直接沿用 cacheTtlMs(默认 15s)。
74
- * 定在 5 分钟的依据:它展示的是「我的仓库 / 待办 PR / Issues」这类分钟级不变的数据,
75
- * 而每次刷新本机实测要 8-13 秒。缓存太短等于让用户反复等这段延迟——实测 60 秒缓存下,
76
- * 用户 65 秒后再打开面板仍需等 9.7 秒。仍随 cacheTtlMs 缩放,用户调大时这里同步变长。
77
- */
78
- const GLOBAL_CACHE_MIN_TTL_MS = 300_000;
79
- /**
80
- * 由用户配置推导全局驾驶舱的缓存时长。
81
- * 抽成导出的纯函数:TTL 的推导规则是测试断言的对象,埋在闭包里就只能靠猜。
82
- */
83
- export function globalCacheTtlMs(cacheTtlMs) {
84
- const base = Number.isFinite(cacheTtlMs) && cacheTtlMs >= 0 ? cacheTtlMs : 15_000;
85
- return Math.max(base * 20, GLOBAL_CACHE_MIN_TTL_MS);
86
- }
87
- /** open_browser 只允许 http/https,杜绝 file:// 与自定义协议被当作打开目标 */
88
- function isSafeExternalUrl(raw) {
89
- if (typeof raw !== 'string' || raw.length === 0 || raw.length > 2048)
90
- return false;
91
- try {
92
- const parsed = new URL(raw);
93
- return parsed.protocol === 'http:' || parsed.protocol === 'https:';
94
- }
95
- catch {
96
- return false;
97
- }
98
- }
99
- export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, cacheTtlMs = 15_000, updateChecker, upgradeManager) {
4
+ import { globalCacheTtlMs, sendJson, readJsonBody } from './route-helpers.js';
5
+ import { registerOverviewRoutes } from './routes-overview.js';
6
+ import { registerActionRoutes } from './routes-actions.js';
7
+ import { registerLifecycleRoutes } from './routes-lifecycle.js';
8
+ import { registerProxyRoutes } from './routes-proxy.js';
9
+ // 再导出历史公共接口:globalCacheTtlMs 曾被测试直接 import,保持路径稳定避免破坏消费方
10
+ export { globalCacheTtlMs };
11
+ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, cacheTtlMs = 15_000, updateChecker, upgradeManager, proxyManager) {
100
12
  const ctx = ctxOrWebServer;
101
13
  const webServer = ctx?.webServer || ctx;
102
14
  if (!webServer || typeof webServer.register !== 'function')
@@ -105,12 +17,6 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
105
17
  const upgrader = upgradeManager || new UpgradeManager();
106
18
  const cacheMap = new TtlCache(cacheTtlMs);
107
19
  const globalCache = new TtlCache(globalCacheTtlMs(cacheTtlMs));
108
- function sendJson(res, status, data) {
109
- res.statusCode = status;
110
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
111
- res.setHeader('Cache-Control', 'no-store');
112
- res.end(JSON.stringify(data));
113
- }
114
20
  /**
115
21
  * 注册单个路由并返回注销器。
116
22
  * 为什么需要前置探测与清理:Cordis 插件在经历热重载、异常崩溃或重新启用时,若先前的 disposer
@@ -140,406 +46,10 @@ export function registerApiRoutes(ctxOrWebServer, executor, workspaceRegistry, c
140
46
  registerOneRoute(route);
141
47
  }
142
48
  };
143
- // 1. 获取全局与当前仓库的 GitHub 概览
144
- bindRoute({
145
- kind: 'exact',
146
- path: '/api/github/overview',
147
- handler: async (req, res) => {
148
- if (req.method !== 'GET') {
149
- sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
150
- return;
151
- }
152
- try {
153
- const url = new URL(req.url || '', 'http://127.0.0.1');
154
- let cwd = url.searchParams.get('cwd');
155
- const sessionId = url.searchParams.get('sessionId');
156
- // 会话 ID → 物理路径,以及「最近活跃工作区」兜底,统一由 workspace 模块提供
157
- if (!cwd && sessionId) {
158
- cwd = findWorkspacePathBySession(sessionId) || null;
159
- }
160
- if (!cwd) {
161
- cwd = findMostRecentWorkspacePath() || null;
162
- }
163
- if (!cwd) {
164
- cwd = process.cwd();
165
- }
166
- const now = Date.now();
167
- const cachedData = cacheMap.get(cwd, now);
168
- if (cachedData) {
169
- sendJson(res, 200, {
170
- ok: true,
171
- data: { ...cachedData, updateInfo: checker.getQuickStatus() },
172
- fromCache: true,
173
- cwd,
174
- });
175
- return;
176
- }
177
- const auth = await executor.checkAuth(cwd);
178
- const repo = await executor.getRepoMetadata(cwd);
179
- let pullRequests = [];
180
- let issues = [];
181
- let runs = [];
182
- if (repo && auth.loggedIn) {
183
- // 并发拉取当前文件夹仓库的 PRs、Issues 和 Runs
184
- const [prsRes, issuesRes, runsRes] = await Promise.all([
185
- executor.run(['pr', 'list', '--json', 'number,title,state,author,headRefName,baseRefName,isDraft,mergeable,reviewDecision,statusCheckRollup,url,updatedAt', '-L', '10'], { cwd, timeoutMs: 8_000 }),
186
- executor.run(['issue', 'list', '--json', 'number,title,state,author,labels,assignees,url,updatedAt', '-L', '10'], { cwd, timeoutMs: 8_000 }),
187
- executor.run(['run', 'list', '--json', 'databaseId,name,status,conclusion,event,headBranch,url,createdAt', '-L', '5'], { cwd, timeoutMs: 8_000 }),
188
- ]);
189
- if (prsRes.ok && Array.isArray(prsRes.data))
190
- pullRequests = prsRes.data;
191
- if (issuesRes.ok && Array.isArray(issuesRes.data)) {
192
- const currentUser = auth.user;
193
- const repoOwner = repo?.owner || repo?.nameWithOwner?.split('/')[0];
194
- issues = issuesRes.data.map((item) => {
195
- const categories = resolveIssueCategories(item, currentUser, repoOwner);
196
- return {
197
- ...item,
198
- categories,
199
- };
200
- });
201
- }
202
- if (runsRes.ok && Array.isArray(runsRes.data))
203
- runs = runsRes.data;
204
- }
205
- const issueStats = calculateIssueStats(issues);
206
- const freshData = {
207
- auth,
208
- repo: repo || undefined,
209
- pullRequests,
210
- issues,
211
- issueStats,
212
- runs,
213
- lastUpdated: new Date().toISOString(),
214
- updateInfo: checker.getQuickStatus(),
215
- };
216
- cacheMap.set(cwd, freshData, now);
217
- sendJson(res, 200, { ok: true, data: freshData, fromCache: false, cwd });
218
- }
219
- catch (err) {
220
- sendJson(res, 500, { ok: false, error: err.message || '获取 GitHub 概览失败' });
221
- }
222
- },
223
- });
224
- // 2. 强制刷新缓存
225
- bindRoute({
226
- kind: 'exact',
227
- path: '/api/github/refresh',
228
- handler: async (req, res) => {
229
- cacheMap.clear();
230
- globalCache.clear();
231
- sendJson(res, 200, { ok: true, message: '缓存已清空,下次读取将拉取最新数据' });
232
- },
233
- });
234
- // 3. 右侧栏快捷操作代理接口
235
- bindRoute({
236
- kind: 'exact',
237
- path: '/api/github/action',
238
- handler: async (req, res) => {
239
- if (req.method !== 'POST') {
240
- sendJson(res, 405, { ok: false, message: '仅支持 POST 请求' });
241
- return;
242
- }
243
- let body = '';
244
- req.on('data', (chunk) => { body += chunk; });
245
- req.on('end', async () => {
246
- try {
247
- const payload = body ? JSON.parse(body) : {};
248
- const cwd = payload.cwd || process.cwd();
249
- switch (payload.action) {
250
- case 'create_pr': {
251
- const cmd = ['pr', 'create', '--title', payload.title || 'Update', '--body', payload.body || 'Created from DSH Rightbar'];
252
- if (payload.draft)
253
- cmd.push('--draft');
254
- const r = await executor.run(cmd, { cwd });
255
- if (!r.ok)
256
- throw new Error(r.error);
257
- sendJson(res, 200, { ok: true, message: 'PR 创建成功', url: r.rawOutput });
258
- break;
259
- }
260
- case 'create_issue': {
261
- const cmd = ['issue', 'create', '--title', payload.title || 'New Issue', '--body', payload.body || 'Created from DSH Rightbar'];
262
- const r = await executor.run(cmd, { cwd });
263
- if (!r.ok)
264
- throw new Error(r.error);
265
- sendJson(res, 200, { ok: true, message: 'Issue 创建成功', url: r.rawOutput });
266
- break;
267
- }
268
- case 'open_browser': {
269
- // 原实现把 payload.url 直接拼进 shell 字符串再交给 exec(),
270
- // 形如 https://x" & calc & " 的 URL 可闭合引号并追加任意命令——本机任意页面
271
- // 都能通过 POST 这个端点触发命令执行。
272
- // 现在改为:先用 URL 解析白名单协议,再用 execFile 参数数组传递,全程不经过 shell。
273
- if (!isSafeExternalUrl(payload.url)) {
274
- throw new Error('url 参数无效:只允许 http/https 开头的完整地址');
275
- }
276
- const { execFile } = await import('node:child_process');
277
- const [bin, args] = process.platform === 'win32'
278
- ? ['cmd', ['/c', 'start', '', payload.url]]
279
- : process.platform === 'darwin'
280
- ? ['open', [payload.url]]
281
- : ['xdg-open', [payload.url]];
282
- // 不 await,也不让打开失败反向影响接口语义;仅记录以便排查
283
- execFile(bin, args, (err) => {
284
- if (err)
285
- console.warn('[dsh-github-flow] 打开浏览器失败:', err?.message || err);
286
- });
287
- sendJson(res, 200, { ok: true, message: '已在浏览器打开' });
288
- break;
289
- }
290
- case 'open_terminal': {
291
- const command = typeof payload.command === 'string' ? payload.command.trim() : 'gh auth login';
292
- // 严格白名单过滤,严防任意命令注入
293
- const allowedCommands = [
294
- 'gh auth login',
295
- 'winget install --id GitHub.cli',
296
- 'brew install gh',
297
- 'sudo apt install gh',
298
- 'sudo dnf install gh',
299
- ];
300
- if (!allowedCommands.includes(command)) {
301
- throw new Error(`不支持唤起该指令: ${command}`);
302
- }
303
- const { execFile } = await import('node:child_process');
304
- let bin = 'cmd';
305
- let args = [];
306
- if (process.platform === 'win32') {
307
- bin = 'cmd';
308
- args = ['/c', 'start', 'powershell', '-NoExit', '-Command', command];
309
- }
310
- else if (process.platform === 'darwin') {
311
- bin = 'osascript';
312
- args = ['-e', `tell application "Terminal" to do script "${command}"`, '-e', 'tell application "Terminal" to activate'];
313
- }
314
- else {
315
- bin = 'x-terminal-emulator';
316
- args = ['-e', command];
317
- }
318
- execFile(bin, args, (err) => {
319
- if (err)
320
- console.warn('[dsh-github-flow] 唤起终端失败:', err?.message || err);
321
- });
322
- sendJson(res, 200, { ok: true, message: '已请求唤起系统终端' });
323
- break;
324
- }
325
- default:
326
- sendJson(res, 400, { ok: false, message: `未知的操作类型: ${payload.action}` });
327
- }
328
- }
329
- catch (err) {
330
- sendJson(res, 500, { ok: false, error: err.message || '操作执行失败' });
331
- }
332
- });
333
- },
334
- });
335
- // 4. 获取全局视角的驾驶舱概览数据 (不绑定单一项目)
336
- bindRoute({
337
- kind: 'exact',
338
- path: '/api/github/global-overview',
339
- handler: async (req, res) => {
340
- if (req.method !== 'GET') {
341
- sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
342
- return;
343
- }
344
- // 全局驾驶舱要 spawn 十几次 gh(认证 + 仓库 + PR + Issue + 每个工作区探测),
345
- // 实测冷启动曾达 18.9 秒。缓存是这里唯一的解药:面板每次挂载都会来取一次。
346
- const now = Date.now();
347
- const cachedGlobal = globalCache.get('global', now);
348
- if (cachedGlobal) {
349
- sendJson(res, 200, {
350
- ok: true,
351
- data: { ...cachedGlobal, updateInfo: checker.getQuickStatus() },
352
- fromCache: true,
353
- });
354
- return;
355
- }
356
- try {
357
- const auth = await executor.checkAuth();
358
- let userRepos = [];
359
- let myPrs = [];
360
- let myIssues = [];
361
- let issueStats = { total: 0, reported: 0, assigned: 0, created: 0 };
362
- let workspaceMatrix = [];
363
- let workspaceError;
364
- if (auth.loggedIn) {
365
- // 并发执行:1. 账号下仓库列表 2. 个人 PRs 3. 三维 Issue 检索(名下仓库用户提单 + 指派待办 + 我创建的)
366
- // 为什么三路并发搜索:
367
- // - --owner=@me: 捕获用户/访客在当前用户所有仓库下提的 Issue (如反馈 Bug、Feature Request),解决外部提单看不见的问题
368
- // - --assignee=@me: 捕获团队成员或自己指派给当前账号的待办任务 (含跨组织/第三方项目)
369
- // - --author=@me: 捕获当前账号亲自发起的任务与向外部提起的议题
370
- // 3 个轻量进程与 repo/prs 同步 spawn,经 OS 并发调度实测仅耗时 3 秒出头,完全满足 8 秒超时门限。
371
- const issueFields = 'number,title,repository,author,assignees,labels,url,updatedAt';
372
- const queryResults = await Promise.allSettled([
373
- executor.run(['repo', 'list', '--limit', '20', '--json', 'name,nameWithOwner,description,defaultBranchRef,isPrivate,stargazerCount,updatedAt,url'], { timeoutMs: 8_000 }),
374
- executor.run(['search', 'prs', '--author=@me', '--state=open', '--limit', '10', '--json', 'number,title,repository,url,updatedAt'], { timeoutMs: 8_000 }),
375
- executor.run(['search', 'issues', '--owner=@me', '--state=open', '--sort=updated', '--limit', '15', '--json', issueFields], { timeoutMs: 8_000 }),
376
- executor.run(['search', 'issues', '--assignee=@me', '--state=open', '--sort=updated', '--limit', '15', '--json', issueFields], { timeoutMs: 8_000 }),
377
- executor.run(['search', 'issues', '--author=@me', '--state=open', '--sort=updated', '--limit', '15', '--json', issueFields], { timeoutMs: 8_000 }),
378
- ]);
379
- const reposRes = queryResults[0].status === 'fulfilled' ? queryResults[0].value : { ok: false };
380
- const prsRes = queryResults[1].status === 'fulfilled' ? queryResults[1].value : { ok: false };
381
- const ownerIssuesRes = queryResults[2].status === 'fulfilled' ? queryResults[2].value : { ok: false };
382
- const assignedIssuesRes = queryResults[3].status === 'fulfilled' ? queryResults[3].value : { ok: false };
383
- const authorIssuesRes = queryResults[4].status === 'fulfilled' ? queryResults[4].value : { ok: false };
384
- if (reposRes.ok && Array.isArray(reposRes.data)) {
385
- userRepos = reposRes.data.map((r) => ({
386
- name: r.name || '',
387
- nameWithOwner: r.nameWithOwner || '',
388
- description: r.description || '',
389
- defaultBranch: r.defaultBranchRef?.name || 'main',
390
- isPrivate: Boolean(r.isPrivate),
391
- stargazerCount: r.stargazerCount || 0,
392
- updatedAt: r.updatedAt || '',
393
- url: r.url || `https://github.com/${r.nameWithOwner}`,
394
- }));
395
- }
396
- if (prsRes.ok && Array.isArray(prsRes.data)) {
397
- myPrs = prsRes.data;
398
- }
399
- // 多源 Issues 聚合、按 URL 唯一去重并排序
400
- const issueMap = new Map();
401
- const collectIssues = (res) => {
402
- if (res.ok && Array.isArray(res.data)) {
403
- for (const item of res.data) {
404
- if (item.url && !issueMap.has(item.url)) {
405
- issueMap.set(item.url, item);
406
- }
407
- }
408
- }
409
- };
410
- collectIssues(ownerIssuesRes);
411
- collectIssues(assignedIssuesRes);
412
- collectIssues(authorIssuesRes);
413
- const rawList = Array.from(issueMap.values());
414
- // 统一按最后更新时间降序排列
415
- rawList.sort((a, b) => new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime());
416
- const currentUser = auth.user;
417
- myIssues = rawList.map((raw) => {
418
- const categories = resolveIssueCategories(raw, currentUser);
419
- return {
420
- number: raw.number,
421
- title: raw.title || '',
422
- repository: raw.repository || { nameWithOwner: '' },
423
- author: raw.author ? { login: raw.author.login } : undefined,
424
- assignees: Array.isArray(raw.assignees) ? raw.assignees.map((a) => ({ login: a.login })) : [],
425
- labels: Array.isArray(raw.labels) ? raw.labels.map((l) => ({ name: l.name, color: l.color })) : [],
426
- url: raw.url || '',
427
- updatedAt: raw.updatedAt || '',
428
- state: raw.state || 'OPEN',
429
- categories,
430
- };
431
- });
432
- issueStats = calculateIssueStats(myIssues);
433
- }
434
- // 解析本地所有工作区与 GitHub 关联矩阵
435
- const wsRead = tryReadWorkspaces();
436
- workspaceError = wsRead.error;
437
- if (!workspaceError) {
438
- const wsEntries = wsRead.workspaces.filter((ws) => ws.path);
439
- // 有界并发探测:每个工作区最多 spawn 2 个 gh 进程,无上限并发会让它们全挤进
440
- // libuv 默认 4 线程的队列里排队,延迟随工作区数量线性劣化。
441
- // 这里刻意只用本地 .git/config 识别身份、不发网络请求——全局矩阵只需要「属于哪个仓库」,
442
- // 每次网络往返都要秒级,5 个工作区就足以把该端点拖到 20 秒。
443
- workspaceMatrix = await mapLimit(wsEntries, WORKSPACE_PROBE_CONCURRENCY, async (ws) => {
444
- const hasGit = hasGitDir(ws.path);
445
- return {
446
- id: ws.id,
447
- title: ws.title || basenameSafe(ws.path),
448
- path: ws.path,
449
- hasGit,
450
- repo: hasGit ? executor.getRepoIdentityFromLocal(ws.path) || undefined : undefined,
451
- sessionCount: ws.sessionIds.length,
452
- };
453
- });
454
- }
455
- const globalData = {
456
- auth,
457
- userRepos,
458
- workspaceMatrix,
459
- myPrs,
460
- myIssues,
461
- issueStats,
462
- lastUpdated: new Date().toISOString(),
463
- updateInfo: checker.getQuickStatus(),
464
- ...(workspaceError ? { workspaceError } : {}),
465
- };
466
- globalCache.set('global', globalData, now);
467
- sendJson(res, 200, { ok: true, data: globalData, fromCache: false });
468
- }
469
- catch (err) {
470
- sendJson(res, 500, { ok: false, error: err.message || '获取全局驾驶舱数据失败' });
471
- }
472
- },
473
- });
474
- // 5. 闲时静默检查更新端点(前端在 gh 业务数据完全拉取就绪后,由 idle 延时静默触发)
475
- bindRoute({
476
- kind: 'exact',
477
- path: '/api/github/check-update',
478
- handler: async (req, res) => {
479
- if (req.method !== 'GET') {
480
- sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
481
- return;
482
- }
483
- try {
484
- const url = new URL(req.url || '', 'http://127.0.0.1');
485
- const force = url.searchParams.get('force') === 'true';
486
- const info = await checker.check(force);
487
- sendJson(res, 200, { ok: true, data: info });
488
- }
489
- catch (err) {
490
- sendJson(res, 500, { ok: false, error: err.message || '检查更新失败' });
491
- }
492
- },
493
- });
494
- // 6. 一键自动升级触发端点(用户点击操作横幅按钮后发起)
495
- bindRoute({
496
- kind: 'exact',
497
- path: '/api/github/upgrade',
498
- handler: async (req, res) => {
499
- if (req.method !== 'POST') {
500
- sendJson(res, 405, { ok: false, message: '仅支持 POST 请求' });
501
- return;
502
- }
503
- try {
504
- const info = await checker.check(false);
505
- const result = await upgrader.executeUpgrade(info.latestVersion);
506
- sendJson(res, result.ok ? 200 : 500, result);
507
- }
508
- catch (err) {
509
- sendJson(res, 500, { ok: false, message: err.message || '执行自动升级异常' });
510
- }
511
- },
512
- });
513
- // 7. 升级状态与实时日志查询端点
514
- bindRoute({
515
- kind: 'exact',
516
- path: '/api/github/upgrade-status',
517
- handler: async (req, res) => {
518
- if (req.method !== 'GET') {
519
- sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
520
- return;
521
- }
522
- sendJson(res, 200, { ok: true, data: upgrader.getStatus() });
523
- },
524
- });
525
- // 8. 快速认证状态轻量探针 (耗时 < 500ms,专供登录自愈检测与快速恢复)
526
- bindRoute({
527
- kind: 'exact',
528
- path: '/api/github/auth',
529
- handler: async (req, res) => {
530
- if (req.method !== 'GET') {
531
- sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
532
- return;
533
- }
534
- try {
535
- const url = new URL(req.url || '', 'http://127.0.0.1');
536
- const force = url.searchParams.get('force') === 'true';
537
- const auth = await executor.checkAuth(undefined, force);
538
- sendJson(res, 200, { ok: true, auth });
539
- }
540
- catch (err) {
541
- sendJson(res, 500, { ok: false, error: err.message || '认证检测失败' });
542
- }
543
- },
544
- });
49
+ // 各分组共享的依赖包:分组内部不再触碰 ctx/webServer 全局
50
+ const deps = { bindRoute, sendJson, readJsonBody };
51
+ registerOverviewRoutes(deps, executor, cacheMap, globalCache, checker);
52
+ registerActionRoutes(deps, executor, cacheMap, globalCache);
53
+ registerLifecycleRoutes(deps, executor, checker, upgrader);
54
+ registerProxyRoutes(deps, checker, proxyManager);
545
55
  }