dsh-github-flow 0.3.0 → 0.5.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 (121) hide show
  1. package/README.en.md +287 -143
  2. package/README.md +270 -125
  3. package/docs/UPGRADE-NOTES.md +12 -9
  4. package/docs/architecture.md +126 -126
  5. package/lib/api/routes-actions.js +249 -0
  6. package/lib/api/routes-overview.js +94 -2
  7. package/lib/cache.d.ts +4 -0
  8. package/lib/cache.js +14 -0
  9. package/lib/client.d.ts +14 -1
  10. package/lib/client.js +4287 -2500
  11. package/lib/commands/gh.d.ts +6 -1
  12. package/lib/commands/gh.js +144 -71
  13. package/lib/executor/git-parser.d.ts +57 -0
  14. package/lib/executor/git-parser.js +255 -0
  15. package/lib/executor/platform-guide.d.ts +14 -0
  16. package/lib/executor/platform-guide.js +40 -0
  17. package/lib/executor/process-utils.d.ts +51 -0
  18. package/lib/executor/process-utils.js +162 -0
  19. package/lib/executor/version-utils.d.ts +12 -0
  20. package/lib/executor/version-utils.js +27 -0
  21. package/lib/executor.d.ts +70 -48
  22. package/lib/executor.js +371 -356
  23. package/lib/git-watcher.d.ts +36 -0
  24. package/lib/git-watcher.js +91 -0
  25. package/lib/main.js +37 -3
  26. package/lib/renderers/agent-task.d.ts +16 -0
  27. package/lib/renderers/agent-task.js +107 -0
  28. package/lib/renderers/cache.d.ts +8 -0
  29. package/lib/renderers/cache.js +66 -0
  30. package/lib/renderers/codespace.d.ts +16 -0
  31. package/lib/renderers/codespace.js +103 -0
  32. package/lib/renderers/common.d.ts +24 -0
  33. package/lib/renderers/common.js +88 -0
  34. package/lib/renderers/discussion.d.ts +16 -0
  35. package/lib/renderers/discussion.js +101 -0
  36. package/lib/renderers/gist.d.ts +15 -0
  37. package/lib/renderers/gist.js +99 -0
  38. package/lib/renderers/issue.d.ts +4 -0
  39. package/lib/renderers/issue.js +70 -0
  40. package/lib/renderers/label.d.ts +4 -0
  41. package/lib/renderers/label.js +48 -0
  42. package/lib/renderers/markdown.d.ts +19 -32
  43. package/lib/renderers/markdown.js +19 -624
  44. package/lib/renderers/pr.d.ts +4 -0
  45. package/lib/renderers/pr.js +107 -0
  46. package/lib/renderers/project.d.ts +20 -0
  47. package/lib/renderers/project.js +128 -0
  48. package/lib/renderers/release.d.ts +4 -0
  49. package/lib/renderers/release.js +74 -0
  50. package/lib/renderers/repo.d.ts +4 -0
  51. package/lib/renderers/repo.js +149 -0
  52. package/lib/renderers/ruleset.d.ts +4 -0
  53. package/lib/renderers/ruleset.js +74 -0
  54. package/lib/renderers/run.d.ts +4 -0
  55. package/lib/renderers/run.js +79 -0
  56. package/lib/renderers/skill.d.ts +34 -0
  57. package/lib/renderers/skill.js +241 -0
  58. package/lib/renderers/status.d.ts +28 -0
  59. package/lib/renderers/status.js +176 -0
  60. package/lib/telemetry.d.ts +70 -0
  61. package/lib/telemetry.js +161 -0
  62. package/lib/tools/agent-task.d.ts +54 -0
  63. package/lib/tools/agent-task.js +135 -0
  64. package/lib/tools/cache.d.ts +64 -0
  65. package/lib/tools/cache.js +186 -0
  66. package/lib/tools/codespace.d.ts +51 -0
  67. package/lib/tools/codespace.js +154 -0
  68. package/lib/tools/discussion.d.ts +70 -0
  69. package/lib/tools/discussion.js +195 -0
  70. package/lib/tools/gist.d.ts +155 -0
  71. package/lib/tools/gist.js +190 -0
  72. package/lib/tools/help.js +214 -13
  73. package/lib/tools/image-utils.d.ts +8 -0
  74. package/lib/tools/image-utils.js +34 -0
  75. package/lib/tools/issue.d.ts +8 -1
  76. package/lib/tools/issue.js +26 -4
  77. package/lib/tools/label.d.ts +80 -0
  78. package/lib/tools/label.js +237 -0
  79. package/lib/tools/pr.d.ts +8 -0
  80. package/lib/tools/pr.js +48 -3
  81. package/lib/tools/project.d.ts +80 -0
  82. package/lib/tools/project.js +241 -0
  83. package/lib/tools/repo.js +13 -1
  84. package/lib/tools/ruleset.d.ts +80 -0
  85. package/lib/tools/ruleset.js +177 -0
  86. package/lib/tools/skill.d.ts +24 -1
  87. package/lib/tools/skill.js +115 -5
  88. package/lib/tools/status.d.ts +40 -0
  89. package/lib/tools/status.js +58 -0
  90. package/lib/types.d.ts +44 -0
  91. package/lib/ui/auth-guide-card.d.ts +4 -0
  92. package/lib/ui/auth-guide-card.js +161 -0
  93. package/lib/ui/branch-selector.d.ts +10 -0
  94. package/lib/ui/branch-selector.js +163 -0
  95. package/lib/ui/commit-modal.d.ts +8 -0
  96. package/lib/ui/commit-modal.js +219 -0
  97. package/lib/ui/help-modal.d.ts +15 -0
  98. package/lib/ui/help-modal.js +202 -0
  99. package/lib/ui/icons.d.ts +7 -0
  100. package/lib/ui/icons.js +53 -0
  101. package/lib/ui/network-banner.d.ts +10 -0
  102. package/lib/ui/network-banner.js +54 -0
  103. package/lib/ui/storage.d.ts +35 -0
  104. package/lib/ui/storage.js +158 -0
  105. package/lib/ui/styles.d.ts +6 -0
  106. package/lib/ui/styles.js +1544 -0
  107. package/lib/ui/sync-settings-modal.d.ts +11 -0
  108. package/lib/ui/sync-settings-modal.js +350 -0
  109. package/lib/ui/sync-status-badge.d.ts +8 -0
  110. package/lib/ui/sync-status-badge.js +64 -0
  111. package/lib/ui/upgrade-card.d.ts +7 -0
  112. package/lib/ui/upgrade-card.js +222 -0
  113. package/lib/ui/utils.d.ts +45 -0
  114. package/lib/ui/utils.js +226 -0
  115. package/lib/ui/workspace-panel.d.ts +4 -0
  116. package/lib/ui/workspace-panel.js +489 -0
  117. package/lib/upgrade-manager.d.ts +5 -1
  118. package/lib/upgrade-manager.js +24 -1
  119. package/lib/version.d.ts +1 -1
  120. package/lib/version.js +1 -1
  121. package/package.json +2 -2
@@ -3,6 +3,8 @@
3
3
  * 共同特征:POST 触发、读少写多、无缓存语义,与概览类路由的演进方向不同。
4
4
  */
5
5
  import { spawn } from 'node:child_process';
6
+ import { hasGitDir } from '../workspace.js';
7
+ import { watchGitMetadata } from '../git-watcher.js';
6
8
  import { isSafeExternalUrl, sendJson } from './route-helpers.js';
7
9
  /**
8
10
  * 构造「在新终端窗口运行命令」的平台命令(execFile 数组形态,不经 shell)。
@@ -226,6 +228,42 @@ export function registerActionRoutes(deps, executor, cacheMap, globalCache) {
226
228
  sendJson(res, 200, { ok: true, message: '已唤起系统终端' });
227
229
  break;
228
230
  }
231
+ case 'switch_branch': {
232
+ const branch = typeof payload.branch === 'string' ? payload.branch.trim() : '';
233
+ if (!branch) {
234
+ sendJson(res, 400, { ok: false, error: '缺少目标分支名称' });
235
+ return;
236
+ }
237
+ const r = await executor.switchGitBranch(cwd, branch);
238
+ if (!r.ok) {
239
+ sendJson(res, 400, { ok: false, error: r.error });
240
+ return;
241
+ }
242
+ cacheMap.clear();
243
+ sendJson(res, 200, { ok: true, currentBranch: r.currentBranch, message: `已成功切换到分支 ${r.currentBranch}` });
244
+ break;
245
+ }
246
+ case 'file_diff':
247
+ case 'get_file_diff': {
248
+ const fileTarget = typeof payload.file === 'string' ? payload.file.trim() : (typeof payload.filePath === 'string' ? payload.filePath.trim() : '');
249
+ if (!fileTarget) {
250
+ sendJson(res, 400, { ok: false, error: '缺少目标文件路径' });
251
+ return;
252
+ }
253
+ const resolvedCwd = executor.resolveWorkspaceCwd(cwd);
254
+ const r = await executor.getFileDiff(resolvedCwd, fileTarget);
255
+ sendJson(res, 200, { ok: r.ok, data: r, error: r.error });
256
+ break;
257
+ }
258
+ case 'uncommitted_files':
259
+ case 'get_uncommitted_files': {
260
+ const limit = Math.min(Math.max(1, Number(payload.limit) || 15), 100);
261
+ const skip = Math.max(0, Number(payload.skip) || 0);
262
+ const resolvedCwd = executor.resolveWorkspaceCwd(cwd);
263
+ const r = await executor.getUncommittedFiles(resolvedCwd, limit, skip);
264
+ sendJson(res, 200, { ok: true, data: r });
265
+ break;
266
+ }
229
267
  default:
230
268
  sendJson(res, 400, { ok: false, message: `未知的操作类型: ${payload.action}` });
231
269
  }
@@ -236,4 +274,215 @@ export function registerActionRoutes(deps, executor, cacheMap, globalCache) {
236
274
  });
237
275
  },
238
276
  });
277
+ // 3. 读取当前工作区的所有 Git 分支列表(纯本地只读,毫秒级响应)
278
+ bindRoute({
279
+ kind: 'exact',
280
+ path: '/api/github/branches',
281
+ handler: async (req, res) => {
282
+ if (req.method !== 'GET') {
283
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
284
+ return;
285
+ }
286
+ try {
287
+ const url = new URL(req.url || '', 'http://127.0.0.1');
288
+ const cwd = url.searchParams.get('cwd') || undefined;
289
+ const result = await executor.getGitBranches(cwd);
290
+ if (!result) {
291
+ sendJson(res, 404, { ok: false, error: '当前工作区不是 Git 仓库或无法读取分支' });
292
+ return;
293
+ }
294
+ sendJson(res, 200, { ok: true, data: result });
295
+ }
296
+ catch (err) {
297
+ sendJson(res, 500, { ok: false, error: err?.message || '读取分支列表失败' });
298
+ }
299
+ },
300
+ });
301
+ // 4. 切换当前工作区的 Git 分支(带未提交变更防护与合法性校验)
302
+ bindRoute({
303
+ kind: 'exact',
304
+ path: '/api/github/branch-switch',
305
+ handler: async (req, res) => {
306
+ if (req.method !== 'POST') {
307
+ sendJson(res, 405, { ok: false, message: '仅支持 POST 请求' });
308
+ return;
309
+ }
310
+ let body = '';
311
+ req.on('data', (chunk) => { body += chunk; });
312
+ req.on('end', async () => {
313
+ try {
314
+ const payload = body ? JSON.parse(body) : {};
315
+ const cwd = payload.cwd || undefined;
316
+ const branch = typeof payload.branch === 'string' ? payload.branch.trim() : '';
317
+ if (!branch) {
318
+ sendJson(res, 400, { ok: false, error: '缺少目标分支名称' });
319
+ return;
320
+ }
321
+ const r = await executor.switchGitBranch(cwd, branch);
322
+ if (!r.ok) {
323
+ sendJson(res, 400, { ok: false, error: r.error });
324
+ return;
325
+ }
326
+ // 核心联动:切换分支成功后,当前工作区缓存立即失效,确保后续拉取新分支数据
327
+ cacheMap.clear();
328
+ sendJson(res, 200, {
329
+ ok: true,
330
+ currentBranch: r.currentBranch,
331
+ message: `已成功切换到分支 ${r.currentBranch}`,
332
+ });
333
+ }
334
+ catch (err) {
335
+ sendJson(res, 500, { ok: false, error: err?.message || '切换分支失败' });
336
+ }
337
+ });
338
+ },
339
+ });
340
+ // 5. 本地 Git 元数据定向监听(专供右侧栏开启「本地 Git 改动实时感知」时长轮询)
341
+ // 严格安全纪律:只针对 .git/index 与 .git/HEAD 实施定向非递归监听,严禁监听源码目录防句柄耗尽;
342
+ // 800ms 防抖合并;连接断开时自动关闭 watcher 释放系统资源(委托给底层 git-watcher 服务)。
343
+ bindRoute({
344
+ kind: 'exact',
345
+ path: '/api/github/git-watch',
346
+ handler: async (req, res) => {
347
+ if (req.method !== 'GET') {
348
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
349
+ return;
350
+ }
351
+ const url = new URL(req.url || '', 'http://127.0.0.1');
352
+ const cwd = url.searchParams.get('cwd');
353
+ if (!cwd || !hasGitDir(cwd)) {
354
+ sendJson(res, 400, { ok: false, error: '非 Git 仓库,无需监听' });
355
+ return;
356
+ }
357
+ let responded = false;
358
+ try {
359
+ const stop = watchGitMetadata(cwd, (event) => {
360
+ if (responded)
361
+ return;
362
+ responded = true;
363
+ sendJson(res, 200, { ok: true, ...event });
364
+ });
365
+ req.on('close', () => {
366
+ if (!responded) {
367
+ responded = true;
368
+ stop();
369
+ }
370
+ });
371
+ }
372
+ catch (err) {
373
+ sendJson(res, 500, { ok: false, error: err?.message || '建立监听失败' });
374
+ }
375
+ },
376
+ });
377
+ // 6. 纯本地 Git 状态毫秒级轻量查询(专供工作区改动时的局部定向刷新)
378
+ // 严格安全与性能纪律:
379
+ // 1. 动静解耦:仅执行本地 getLocalGitStatus、getGitCommitLog 与 getGitBranches;
380
+ // 2. 100% 零网络调用、零远端 GitHub API 请求、零 Token 额度消耗,耗时仅 5~10ms;
381
+ // 3. 彻底与 PR/Issue/Run 远端数据解耦,切断因修改本地文件误触发远端全量网络风暴与超时报错的恶性循环。
382
+ bindRoute({
383
+ kind: 'exact',
384
+ path: '/api/github/local-git-status',
385
+ handler: async (req, res) => {
386
+ if (req.method !== 'GET') {
387
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
388
+ return;
389
+ }
390
+ const url = new URL(req.url || '', 'http://127.0.0.1');
391
+ const cwd = url.searchParams.get('cwd');
392
+ if (!cwd || !hasGitDir(cwd)) {
393
+ sendJson(res, 400, { ok: false, error: '非 Git 仓库,无法获取本地 Git 状态' });
394
+ return;
395
+ }
396
+ try {
397
+ const gitStatus = await executor.getLocalGitStatus(cwd).catch(() => null);
398
+ const commitLog = await executor
399
+ .getGitCommitLog(cwd, gitStatus ? gitStatus.hasUpstream : false)
400
+ .catch(() => null);
401
+ const branches = await executor.getGitBranches(cwd).catch(() => null);
402
+ // 同步就地更新内存缓存中的本地元数据,保证后续全量请求也能自然拿到最新的本地状态
403
+ const cached = cacheMap.getFallback(cwd) || cacheMap.get(cwd, Date.now());
404
+ if (cached) {
405
+ if (gitStatus)
406
+ cached.gitStatus = gitStatus;
407
+ if (commitLog)
408
+ cached.commitLog = commitLog;
409
+ if (branches)
410
+ cached.branches = branches;
411
+ }
412
+ sendJson(res, 200, {
413
+ ok: true,
414
+ data: {
415
+ gitStatus,
416
+ commitLog,
417
+ branches,
418
+ },
419
+ cwd,
420
+ });
421
+ }
422
+ catch (err) {
423
+ sendJson(res, 500, { ok: false, error: err?.message || '获取本地 Git 状态失败' });
424
+ }
425
+ },
426
+ });
427
+ // 7. 单个未提交文件的 Git Diff 差异查询(供右侧栏 FileDiffModal 调用)
428
+ bindRoute({
429
+ kind: 'exact',
430
+ path: '/api/github/file-diff',
431
+ handler: async (req, res) => {
432
+ if (req.method !== 'GET') {
433
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
434
+ return;
435
+ }
436
+ try {
437
+ const url = new URL(req.url || '', 'http://127.0.0.1');
438
+ const cwd = url.searchParams.get('cwd') || undefined;
439
+ const file = url.searchParams.get('file');
440
+ if (!file || !file.trim()) {
441
+ sendJson(res, 400, { ok: false, error: '缺少必需的 file 参数' });
442
+ return;
443
+ }
444
+ const resolvedCwd = executor.resolveWorkspaceCwd(cwd);
445
+ if (!hasGitDir(resolvedCwd)) {
446
+ sendJson(res, 400, { ok: false, error: '当前工作区不是 Git 仓库' });
447
+ return;
448
+ }
449
+ const diffResult = await executor.getFileDiff(resolvedCwd, file.trim());
450
+ if (!diffResult.ok) {
451
+ sendJson(res, 400, { ok: false, error: diffResult.error || '获取文件差异失败' });
452
+ return;
453
+ }
454
+ sendJson(res, 200, { ok: true, data: diffResult });
455
+ }
456
+ catch (err) {
457
+ sendJson(res, 500, { ok: false, error: err?.message || '获取文件差异失败' });
458
+ }
459
+ },
460
+ });
461
+ // 8. 本地未提交文件列表轻量流式分页查询(供右侧栏滑动加载调用)
462
+ bindRoute({
463
+ kind: 'exact',
464
+ path: '/api/github/uncommitted-files',
465
+ handler: async (req, res) => {
466
+ if (req.method !== 'GET') {
467
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
468
+ return;
469
+ }
470
+ try {
471
+ const url = new URL(req.url || '', 'http://127.0.0.1');
472
+ const cwd = url.searchParams.get('cwd') || undefined;
473
+ const limit = Math.min(Math.max(1, Number(url.searchParams.get('limit')) || 15), 100);
474
+ const skip = Math.max(0, Number(url.searchParams.get('skip')) || 0);
475
+ const resolvedCwd = executor.resolveWorkspaceCwd(cwd);
476
+ if (!hasGitDir(resolvedCwd)) {
477
+ sendJson(res, 400, { ok: false, error: '当前工作区不是 Git 仓库' });
478
+ return;
479
+ }
480
+ const result = await executor.getUncommittedFiles(resolvedCwd, limit, skip);
481
+ sendJson(res, 200, { ok: true, data: result });
482
+ }
483
+ catch (err) {
484
+ sendJson(res, 500, { ok: false, error: err?.message || '获取未提交文件列表失败' });
485
+ }
486
+ },
487
+ });
239
488
  }
@@ -17,6 +17,7 @@ export function registerOverviewRoutes(deps, executor, cacheMap, globalCache, ch
17
17
  const url = new URL(req.url || '', 'http://127.0.0.1');
18
18
  let cwd = url.searchParams.get('cwd');
19
19
  const sessionId = url.searchParams.get('sessionId');
20
+ const force = url.searchParams.get('force') === 'true';
20
21
  // 会话 ID → 物理路径,以及「最近活跃工作区」兜底,统一由 workspace 模块提供
21
22
  if (!cwd && sessionId) {
22
23
  cwd = findWorkspacePathBySession(sessionId) || null;
@@ -29,7 +30,7 @@ export function registerOverviewRoutes(deps, executor, cacheMap, globalCache, ch
29
30
  }
30
31
  const now = Date.now();
31
32
  const cachedData = cacheMap.get(cwd, now);
32
- if (cachedData) {
33
+ if (cachedData && !force) {
33
34
  sendJson(res, 200, {
34
35
  ok: true,
35
36
  data: { ...cachedData, updateInfo: checker.getQuickStatus() },
@@ -39,7 +40,25 @@ export function registerOverviewRoutes(deps, executor, cacheMap, globalCache, ch
39
40
  return;
40
41
  }
41
42
  const auth = await executor.checkAuth(cwd);
42
- const repo = await executor.getRepoMetadata(cwd);
43
+ // 先行检查当前工作区是否真实包含 .git,无 .git 铁证判定为未关联仓库
44
+ const isGitRepo = hasGitDir(cwd);
45
+ const repo = isGitRepo ? await executor.getRepoMetadata(cwd) : null;
46
+ // 若当前工作区未关联 Git 仓库,直接返回纯净空状态,坚决不拉取远端数据
47
+ if (!isGitRepo || !repo) {
48
+ const emptyData = {
49
+ auth,
50
+ repo: undefined,
51
+ pullRequests: [],
52
+ issues: [],
53
+ issueStats: { total: 0, reported: 0, assigned: 0, created: 0 },
54
+ runs: [],
55
+ lastUpdated: new Date().toISOString(),
56
+ updateInfo: checker.getQuickStatus(),
57
+ };
58
+ cacheMap.set(cwd, emptyData, now);
59
+ sendJson(res, 200, { ok: true, data: emptyData, fromCache: false, cwd });
60
+ return;
61
+ }
43
62
  let pullRequests = [];
44
63
  let issues = [];
45
64
  let runs = [];
@@ -53,6 +72,52 @@ export function registerOverviewRoutes(deps, executor, cacheMap, globalCache, ch
53
72
  executor.run(['issue', 'list', '--json', 'number,title,state,author,labels,assignees,url,updatedAt', '-L', '10'], { cwd, timeoutMs: 8_000 }),
54
73
  executor.run(['run', 'list', '--json', 'databaseId,name,status,conclusion,event,headBranch,url,createdAt', '-L', '5'], { cwd, timeoutMs: 8_000 }),
55
74
  ]);
75
+ // 核心数据拉取故障检测:若拉取核心业务数据遭遇失败(网络超时/断网/接口报错),
76
+ // 绝对不能把失败伪装成「0 个 PR、0 个 Issue」并假装成功返回!
77
+ const failures = [];
78
+ if (!prsRes.ok)
79
+ failures.push(`PR 获取失败: ${prsRes.error || '网络或接口异常'}`);
80
+ if (!issuesRes.ok)
81
+ failures.push(`Issue 获取失败: ${issuesRes.error || '网络或接口异常'}`);
82
+ if (!runsRes.ok)
83
+ failures.push(`Actions 获取失败: ${runsRes.error || '网络或接口异常'}`);
84
+ if (failures.length > 0) {
85
+ const failureReason = failures.join(';');
86
+ // 若此前内存有缓存,保全已有缓存内容,附加 networkError 与 partialFailure
87
+ const fallbackCached = cacheMap.getFallback(cwd) || cacheMap.get(cwd, now);
88
+ if (fallbackCached && (fallbackCached.pullRequests.length > 0 || fallbackCached.issues.length > 0 || fallbackCached.runs.length > 0)) {
89
+ sendJson(res, 200, {
90
+ ok: true,
91
+ fromCache: true,
92
+ partialFailure: true,
93
+ error: failureReason,
94
+ data: {
95
+ ...fallbackCached,
96
+ networkError: true,
97
+ networkErrorReason: failureReason,
98
+ auth: { ...fallbackCached.auth, networkError: true, networkErrorReason: failureReason },
99
+ },
100
+ cwd,
101
+ });
102
+ return;
103
+ }
104
+ else {
105
+ const freshData = {
106
+ auth: { ...auth, networkError: true, networkErrorReason: failureReason },
107
+ repo: repo || undefined,
108
+ pullRequests: [],
109
+ issues: [],
110
+ issueStats: { total: 0, reported: 0, assigned: 0, created: 0 },
111
+ runs: [],
112
+ networkError: true,
113
+ networkErrorReason: failureReason,
114
+ lastUpdated: new Date().toISOString(),
115
+ updateInfo: checker.getQuickStatus(),
116
+ };
117
+ sendJson(res, 200, { ok: true, partialFailure: true, error: failureReason, data: freshData, cwd });
118
+ return;
119
+ }
120
+ }
56
121
  if (prsRes.ok && Array.isArray(prsRes.data))
57
122
  pullRequests = prsRes.data;
58
123
  if (issuesRes.ok && Array.isArray(issuesRes.data)) {
@@ -96,6 +161,8 @@ export function registerOverviewRoutes(deps, executor, cacheMap, globalCache, ch
96
161
  freshExtras.commitLog = await executor
97
162
  .getGitCommitLog(cwd, gitStatus ? gitStatus.hasUpstream : false)
98
163
  .catch(() => null);
164
+ // 5. 分支列表(纯本地毫秒级):供右侧栏分支选择器 0ms 秒开,消除独立请求延迟与 404 风险
165
+ freshExtras.branches = await executor.getGitBranches(cwd).catch(() => null);
99
166
  }
100
167
  const issueStats = calculateIssueStats(issues);
101
168
  const freshData = {
@@ -166,6 +233,31 @@ export function registerOverviewRoutes(deps, executor, cacheMap, globalCache, ch
166
233
  const ownerIssuesRes = queryResults[2].status === 'fulfilled' ? queryResults[2].value : { ok: false };
167
234
  const assignedIssuesRes = queryResults[3].status === 'fulfilled' ? queryResults[3].value : { ok: false };
168
235
  const authorIssuesRes = queryResults[4].status === 'fulfilled' ? queryResults[4].value : { ok: false };
236
+ // 检测全局拉取是否发生网络故障,绝不把网络失败当作「0 仓库、0 PR」伪造成功
237
+ const globalFailures = [];
238
+ if (!reposRes.ok)
239
+ globalFailures.push(`仓库列表拉取失败: ${reposRes.error || '网络或接口异常'}`);
240
+ if (!prsRes.ok)
241
+ globalFailures.push(`个人 PR 拉取失败: ${prsRes.error || '网络或接口异常'}`);
242
+ if (globalFailures.length > 0) {
243
+ const globalNetReason = globalFailures.join(';');
244
+ const fallbackGlobal = globalCache.getFallback('global') || globalCache.get('global', now);
245
+ if (fallbackGlobal) {
246
+ sendJson(res, 200, {
247
+ ok: true,
248
+ fromCache: true,
249
+ partialFailure: true,
250
+ error: globalNetReason,
251
+ data: {
252
+ ...fallbackGlobal,
253
+ networkError: true,
254
+ networkErrorReason: globalNetReason,
255
+ auth: { ...fallbackGlobal.auth, networkError: true, networkErrorReason: globalNetReason },
256
+ },
257
+ });
258
+ return;
259
+ }
260
+ }
169
261
  if (reposRes.ok && Array.isArray(reposRes.data)) {
170
262
  userRepos = reposRes.data.map((r) => ({
171
263
  name: r.name || '',
package/lib/cache.d.ts CHANGED
@@ -8,6 +8,8 @@ export declare class TtlCache<T> {
8
8
  private ttlMs;
9
9
  private maxEntries;
10
10
  private store;
11
+ /** 容灾备份底座:记录最后一次成功的有效快照,不受 TTL 过期或主动 refresh 清空的影响 */
12
+ private fallbackStore;
11
13
  /**
12
14
  * @param ttlMs 命中有效期;由面向用户的 cacheTtlMs 配置驱动
13
15
  * @param maxEntries 容量上限。键通常是工作区路径,一次会话就可能引入若干键,
@@ -16,6 +18,8 @@ export declare class TtlCache<T> {
16
18
  constructor(ttlMs: number, maxEntries?: number);
17
19
  get(key: string, now: number): T | undefined;
18
20
  set(key: string, data: T, now: number): void;
21
+ /** 获取最后一次有效成功的容灾底座数据(专供网络故障、超时时的 Fail-Safe 保全) */
22
+ getFallback(key: string): T | undefined;
19
23
  get size(): number;
20
24
  clear(): void;
21
25
  }
package/lib/cache.js CHANGED
@@ -8,6 +8,8 @@ export class TtlCache {
8
8
  ttlMs;
9
9
  maxEntries;
10
10
  store = new Map();
11
+ /** 容灾备份底座:记录最后一次成功的有效快照,不受 TTL 过期或主动 refresh 清空的影响 */
12
+ fallbackStore = new Map();
11
13
  /**
12
14
  * @param ttlMs 命中有效期;由面向用户的 cacheTtlMs 配置驱动
13
15
  * @param maxEntries 容量上限。键通常是工作区路径,一次会话就可能引入若干键,
@@ -32,17 +34,29 @@ export class TtlCache {
32
34
  }
33
35
  set(key, data, now) {
34
36
  this.store.set(key, { data, time: now });
37
+ this.fallbackStore.set(key, data);
35
38
  while (this.store.size > this.maxEntries) {
36
39
  const oldest = this.store.keys().next();
37
40
  if (oldest.done)
38
41
  break;
39
42
  this.store.delete(oldest.value);
40
43
  }
44
+ while (this.fallbackStore.size > this.maxEntries) {
45
+ const oldest = this.fallbackStore.keys().next();
46
+ if (oldest.done)
47
+ break;
48
+ this.fallbackStore.delete(oldest.value);
49
+ }
50
+ }
51
+ /** 获取最后一次有效成功的容灾底座数据(专供网络故障、超时时的 Fail-Safe 保全) */
52
+ getFallback(key) {
53
+ return this.fallbackStore.get(key);
41
54
  }
42
55
  get size() {
43
56
  return this.store.size;
44
57
  }
45
58
  clear() {
59
+ // 清空活动缓存以迫使后续请求重新发起网络拉取,但保留 fallbackStore 用于网络故障容灾
46
60
  this.store.clear();
47
61
  }
48
62
  }
package/lib/client.d.ts CHANGED
@@ -8,4 +8,17 @@ declare global {
8
8
  };
9
9
  }
10
10
  }
11
- export {};
11
+ import { formatRelativeTime, rollupCiStatus, reviewDecisionBadge, eventLabel, parseGitStatText } from './ui/utils.js';
12
+ import { GITHUB_PANEL_CSS, injectStyles } from './ui/styles.js';
13
+ import { renderOctocat, renderGitIcon, renderOctocatBadge, renderPluginBadge } from './ui/icons.js';
14
+ import { getLocalSyncSettings, saveLocalSyncSettings, getStoredGlobalOverview, saveStoredGlobalOverview, clearStoredGlobalOverview, getCacheStorageMeta, getRightbarStorageKey, getStoredRightbarSnapshot, saveStoredRightbarData, getStoredHeaderCardsVisible, saveStoredHeaderCardsVisible, HEADER_CARDS_STORAGE_KEY } from './ui/storage.js';
15
+ import { BranchSelectorDropdown } from './ui/branch-selector.js';
16
+ import { AuthGuideCard } from './ui/auth-guide-card.js';
17
+ import { UpgradeNotificationCard } from './ui/upgrade-card.js';
18
+ import { NetworkAlertBanner } from './ui/network-banner.js';
19
+ import { SyncStatusBadge } from './ui/sync-status-badge.js';
20
+ import { CommitDetailModal } from './ui/commit-modal.js';
21
+ import { GlobalSyncSettingsModal } from './ui/sync-settings-modal.js';
22
+ import { GithubHelpModal } from './ui/help-modal.js';
23
+ import { GithubWorkspacePanel } from './ui/workspace-panel.js';
24
+ export { formatRelativeTime, rollupCiStatus, reviewDecisionBadge, eventLabel, parseGitStatText, GITHUB_PANEL_CSS, injectStyles, renderOctocat, renderGitIcon, renderOctocatBadge, renderPluginBadge, getLocalSyncSettings, saveLocalSyncSettings, getStoredGlobalOverview, saveStoredGlobalOverview, clearStoredGlobalOverview, getCacheStorageMeta, getRightbarStorageKey, getStoredRightbarSnapshot, saveStoredRightbarData, getStoredHeaderCardsVisible, saveStoredHeaderCardsVisible, HEADER_CARDS_STORAGE_KEY, BranchSelectorDropdown, AuthGuideCard, UpgradeNotificationCard, NetworkAlertBanner, SyncStatusBadge, CommitDetailModal, GlobalSyncSettingsModal, GithubHelpModal, GithubWorkspacePanel, };