dsh-github-flow 0.3.0 → 0.4.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 (94) hide show
  1. package/README.en.md +220 -143
  2. package/README.md +202 -124
  3. package/docs/UPGRADE-NOTES.md +14 -8
  4. package/docs/architecture.md +111 -129
  5. package/lib/api/routes-actions.js +117 -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 +13 -1
  10. package/lib/client.js +2906 -2475
  11. package/lib/executor/git-parser.d.ts +25 -0
  12. package/lib/executor/git-parser.js +143 -0
  13. package/lib/executor/platform-guide.d.ts +14 -0
  14. package/lib/executor/platform-guide.js +40 -0
  15. package/lib/executor/process-utils.d.ts +51 -0
  16. package/lib/executor/process-utils.js +162 -0
  17. package/lib/executor/version-utils.d.ts +12 -0
  18. package/lib/executor/version-utils.js +27 -0
  19. package/lib/executor.d.ts +45 -48
  20. package/lib/executor.js +184 -353
  21. package/lib/git-watcher.d.ts +36 -0
  22. package/lib/git-watcher.js +91 -0
  23. package/lib/main.js +10 -0
  24. package/lib/renderers/cache.d.ts +8 -0
  25. package/lib/renderers/cache.js +66 -0
  26. package/lib/renderers/common.d.ts +24 -0
  27. package/lib/renderers/common.js +88 -0
  28. package/lib/renderers/gist.d.ts +15 -0
  29. package/lib/renderers/gist.js +99 -0
  30. package/lib/renderers/issue.d.ts +4 -0
  31. package/lib/renderers/issue.js +70 -0
  32. package/lib/renderers/label.d.ts +4 -0
  33. package/lib/renderers/label.js +48 -0
  34. package/lib/renderers/markdown.d.ts +15 -32
  35. package/lib/renderers/markdown.js +15 -624
  36. package/lib/renderers/pr.d.ts +4 -0
  37. package/lib/renderers/pr.js +107 -0
  38. package/lib/renderers/release.d.ts +4 -0
  39. package/lib/renderers/release.js +74 -0
  40. package/lib/renderers/repo.d.ts +4 -0
  41. package/lib/renderers/repo.js +149 -0
  42. package/lib/renderers/ruleset.d.ts +4 -0
  43. package/lib/renderers/ruleset.js +74 -0
  44. package/lib/renderers/run.d.ts +4 -0
  45. package/lib/renderers/run.js +79 -0
  46. package/lib/renderers/skill.d.ts +4 -0
  47. package/lib/renderers/skill.js +61 -0
  48. package/lib/renderers/status.d.ts +23 -0
  49. package/lib/renderers/status.js +124 -0
  50. package/lib/tools/cache.d.ts +64 -0
  51. package/lib/tools/cache.js +186 -0
  52. package/lib/tools/gist.d.ts +155 -0
  53. package/lib/tools/gist.js +190 -0
  54. package/lib/tools/help.js +107 -6
  55. package/lib/tools/image-utils.d.ts +8 -0
  56. package/lib/tools/image-utils.js +34 -0
  57. package/lib/tools/issue.d.ts +8 -1
  58. package/lib/tools/issue.js +26 -4
  59. package/lib/tools/label.d.ts +80 -0
  60. package/lib/tools/label.js +237 -0
  61. package/lib/tools/pr.d.ts +8 -0
  62. package/lib/tools/pr.js +48 -3
  63. package/lib/tools/ruleset.d.ts +80 -0
  64. package/lib/tools/ruleset.js +177 -0
  65. package/lib/tools/status.d.ts +39 -0
  66. package/lib/tools/status.js +53 -0
  67. package/lib/types.d.ts +39 -0
  68. package/lib/ui/auth-guide-card.d.ts +4 -0
  69. package/lib/ui/auth-guide-card.js +161 -0
  70. package/lib/ui/branch-selector.d.ts +10 -0
  71. package/lib/ui/branch-selector.js +163 -0
  72. package/lib/ui/commit-modal.d.ts +8 -0
  73. package/lib/ui/commit-modal.js +89 -0
  74. package/lib/ui/icons.d.ts +7 -0
  75. package/lib/ui/icons.js +53 -0
  76. package/lib/ui/network-banner.d.ts +10 -0
  77. package/lib/ui/network-banner.js +54 -0
  78. package/lib/ui/storage.d.ts +26 -0
  79. package/lib/ui/storage.js +134 -0
  80. package/lib/ui/styles.d.ts +6 -0
  81. package/lib/ui/styles.js +1285 -0
  82. package/lib/ui/sync-settings-modal.d.ts +11 -0
  83. package/lib/ui/sync-settings-modal.js +350 -0
  84. package/lib/ui/sync-status-badge.d.ts +8 -0
  85. package/lib/ui/sync-status-badge.js +64 -0
  86. package/lib/ui/upgrade-card.d.ts +7 -0
  87. package/lib/ui/upgrade-card.js +197 -0
  88. package/lib/ui/utils.d.ts +10 -0
  89. package/lib/ui/utils.js +66 -0
  90. package/lib/ui/workspace-panel.d.ts +4 -0
  91. package/lib/ui/workspace-panel.js +464 -0
  92. package/lib/version.d.ts +1 -1
  93. package/lib/version.js +1 -1
  94. package/package.json +1 -1
@@ -25,55 +25,65 @@
25
25
  │ │ - AI_AGENT=dsh 注入 [gh 官方 agent 识别·可配置开关] │ │
26
26
  │ │ - UpdateChecker (源可选探测 + 24h 缓存 + Fail-Safe) │ │
27
27
  │ │ - UpgradeManager (跨平台自适应 + 互斥锁 + macOS PATH 补全) │ │
28
- │ │ - spawnWithStdin (密文通道: spawn+stdin.end + kill 超时) │ │
28
+ │ │ - ProxyManager (环境变量集中路由 + 统一网络分发) │ │
29
+ │ │ - 模块化子系统 (src/executor/): │ │
30
+ │ │ · git-parser.ts (无 I/O 纯文本解析纯函数) │ │
31
+ │ │ · version-utils.ts (语义化版本比较纯函数) │ │
32
+ │ │ · process-utils.ts (spawnWithStdin 密文流控与字节截断) │ │
33
+ │ │ · platform-guide.ts (跨平台安装引导与升级建议) │ │
29
34
  │ └──────┬───────────────────────┬────────────────────────┬──────┘ │
30
35
  │ │ │ │ │
31
36
  │ ▼ ▼ ▼ │
32
37
  │ ┌──────────────────┐ ┌──────────────────┐ ┌───────────────────────┐ │
33
- │ │ 9 大 Agent 工具 │ │ 人类 Slash 指令 │ │ Host HTTP WebServer │ │
38
+ │ │ 14 大 Agent 工具 │ │ 人类 Slash 指令 │ │ Host HTTP WebServer │ │
34
39
  │ │ (defineTool DSL)│ │ /gh status │ │ (Effect Disposer 清理)│ │
35
- │ │ - pr (+ready· │ │ /gh upgrade │ │ - /api/github/overview│ │
36
- │ │ revert·update)│ │ /gh help │ │ - /api/github/action │ │
37
- │ │ - issue (+delete│ └──────────────────┘ │ - /api/github/refresh │ │
38
- │ │ ·transfer·pin·│ │ - /check-update (并行)│ │
39
- │ │ lock·status) │ │ - /upgrade (一键自动) │ │
40
- │ │ - run (+workflow│ │ - /update-settings │ │
41
- │ │ 系列·download)│ (workflow_enable/disable │ │ - /proxy (全局代理) │ │
42
- │ │ - repo (+search │ 启停·list/view·download) │ - /auth (轻量探针) │ │
43
- │ │ 三域+read_ │ └──────────┬────────────┘ │
44
- │ │ file &read_dir)│ │ │
45
- │ │ - api (逃生门) │ (search 三域=issues/prs/commits; │ │
46
- │ │ - release │ read_file/read_dir 免clone读取) │ │
47
- │ │ - secret (stdin │ │ │
48
- │ │ +variable 域) │ │ │
49
- │ │ - skill │ │ │
50
- │ │ - help (元指南) │ │ │
51
- │ └────────┬─────────┘ │ │
52
- │ │ │ │
53
- │ ▼ │ │
54
- │ ┌──────────────────────────────────┐ │ │
55
- │ │ Markdown 渲染引擎 (renderers/) │ │ │
56
- │ │ - 列表表格化压缩 70%+ Token │ │ │
57
- │ │ - 状态徽章/超链接/转义防破损 │ │ │
58
- │ │ - 高危确认卡片/敏感字段隔离 │ │ │
59
- │ └──────────────────────────────────┘ │ │
60
- └─────────────────────────────────────────────────────────────┼───────────────┘
40
+ │ │ - pr (+attach) │ │ /gh upgrade │ │ - /api/github/overview│ │
41
+ │ │ - issue (+attach│ │ /gh help │ │ - /api/github/action │ │
42
+ │ │ - run (workflow)│ └──────────────────┘ │ - /api/github/refresh │ │
43
+ │ │ - repo (read/sr)│ │ - /api/github/branches│ │
44
+ │ │ - api (逃生门) │ (workflow_enable/disable │ │ - /branch-switch │ │
45
+ │ │ - release │ 启停·list/view·download) │ - /api/github/git- │ │
46
+ │ │ - secret & var │ │ watch (定向监听) │ │
47
+ │ │ - skill │ (search 三域=issues/prs/ │ - /check-update (并行)│ │
48
+ │ │ - label (CRUD/cl│ commits; read_file/dir) │ - /upgrade (一键自动) │ │
49
+ │ │ - ruleset(check)│ │ - /update-settings │ │
50
+ │ │ - cache (治理) │ │ - /proxy (全局代理) │ │
51
+ │ │ - gist (分享) │ │ - /auth (轻量探针) │ │
52
+ │ │ - status (大盘) │ └──────────┬────────────┘ │
53
+ │ │ - help (元指南) │ │ │
54
+ │ └────────┬─────────┘ ▼ │
55
+ │ │ ┌───────────────────────┐ │
56
+ │ │ │ git-watcher (独立服务)│ │
57
+ │ ▼ │ - .git/HEAD & index │ │
58
+ │ ┌──────────────────────────────────┐ │ - 800ms 防抖/35s 超时 │ │
59
+ │ │ Markdown 渲染引擎 (renderers/) │ └───────────────────────┘ │
60
+ │ │ - common.ts: 单元格清洗/通用徽章 │ │
61
+ │ │ - 11领域模块: pr/issue/run/repo/ │ │
62
+ │ │ release/skill/label/ruleset/ │ │
63
+ │ │ cache/gist/status 独立渲染 │ │
64
+ │ │ - 统一门面出口 markdown.ts 兼容 │ │
65
+ │ └──────────────────────────────────┘ │
66
+ └─────────────────────────────────────────────────────────────┬───────────────┘
61
67
  │
62
68
  ┌─────────────────────────────────────────────────────────────┼───────────────┐
63
69
  │ DSH Web Client ▼ │
64
70
  │ │
65
- │ ┌──────────────────────────────────┐ ┌─────────────────────────────┐ │
66
- │ │ 全局驾驶舱 (Main Slot) │ │ 右侧栏面板 (Right Sidebar) │ │
67
- │ │ - 账号全局所有 GitHub 仓库 │ │ - 五 Tab 互斥 (Git/未提交/ │ │
68
- │ │ - 本地工作区矩阵透视 │ │ PR/Issues/Actions) │ │
69
- │ │ - 三维待办 Issues (药丸筛选) │ │ - Git 记录与分支同步全景 │ │
70
- │ │ - 用户提单 / 分配待办 / 我发起 │ │ - 提交详情居中弹窗 (可滑动)│ │
71
- │ │ - 顶部升级通知横幅 (Banner) │ │ - 下滑自动分页加载更多 │ │
72
- │ │ - 动态数据新鲜度胶囊与 SWR 缓存│ │ - 快捷操作网格与设置入口 │ │
73
- │ │ - 四分区设置弹窗 (同步/代理/ │ │ - 官方标准 Git 矢量图标 │ │
74
- │ │ 更新源/关于) │ │ - 「插件」身份徽章 (直达) │ │
75
- │ │ - 「插件」身份徽章 (关于直达) │ │ │ │
76
- │ └──────────────────────────────────┘ └─────────────────────────────┘ │
71
+ │ ┌─────────────────────────────────────────────────────────────────────┐ │
72
+ │ │ 模块化 UI 架构 (src/ui/ ➔ 经 scripts/build.mjs 拓扑聚合单一 Bundle) │ │
73
+ │ │ │ │
74
+ │ │ · styles.ts (1290行自适应 CSS, 构建期紧凑压缩) │ │
75
+ │ │ · icons.ts (Octocat, Git 官方菱形分支矢量组件, 插件徽标) │ │
76
+ │ │ · storage.ts (LocalStorage SWR 状态机与偏好存储) │ │
77
+ │ │ · components/ (分支下拉/认证引导/升级通知/离线横幅/状态胶囊) │ │
78
+ │ │ · modals/ (Commit 详情弹窗 / 4分区全局设置弹窗) │ │
79
+ │ │ · panels/ (首页全景大面板 workspace-panel.ts) │ │
80
+ │ │ │ │
81
+ │ │ 核心组装入口 (src/client.ts, ~1,260 行高内聚): │ │
82
+ │ │ - applyTabState (纯 DOM 互斥状态应用, 满足单测白盒契约) │ │
83
+ │ │ - renderGithubRepoView (五 Tab 核心视图: Git/未提交/PR/Issue/Run) │ │
84
+ │ │ - GithubRightbarBody (右侧栏主体, 状态引用解耦与时序安全守护) │ │
85
+ │ │ - apply(ctx) (Cordis Client 插件安装入口与槽位注入) │ │
86
+ │ └─────────────────────────────────────────────────────────────────────┘ │
77
87
  └─────────────────────────────────────────────────────────────────────────────┘
78
88
  ```
79
89
 
@@ -93,100 +103,72 @@
93
103
 
94
104
  ### 2.3 上下文 Token 保护与防御性输出包装 (Token Budget Guard)
95
105
  - 所有的 `gh` 查询原生采用 `--json <fields>` 结构化投影;
96
- - 对长文本输出(如大型 PR 的完整 `diff`、CI 失败步骤日志)设置安全截断阈值(默认 24KB),采用码点安全切割与二分查找字节截断,保留头尾关键上下文并添加显式截断标记,杜绝撑爆模型上下文;
97
- - 列表与详情输出经独立 Markdown 渲染引擎(`src/renderers/markdown.ts`)转化为高密度表格与卡片(带状态徽章、可点击超链接与竖线转义防护),实测 20 条 PR 列表输出压缩 70%+ Token;
106
+ - 对长文本输出(如大型 PR 的完整 `diff`、CI 失败步骤日志)设置安全截断阈值(默认 24KB),采用码点安全切割与二分查找字节截断(`src/executor/process-utils.ts`),保留头尾关键上下文并添加显式截断标记,杜绝撑爆模型上下文;
107
+ - 列表与详情输出经独立 Markdown 渲染引擎(`src/renderers/` 7 大领域模块)转化为高密度表格与卡片(带状态徽章、可点击超链接与竖线转义防护),实测 20 条 PR 列表输出压缩 70%+ Token;
98
108
  - 所有 Agent 工具的输出统一经过归一化,列表自动打包为 `{ items, count }`,标量打包为 `{ result }`,100% 严丝合缝契合 DSH 工具网关模式约束;
99
- - **gh 版本守卫降级链(runGuarded)**:preview 命令与新特性按 `minVersion` 门槛路由——版本达标直通主命令、不足走 `fallback` 降级链路(结果携带 `degraded/degradedReason` 结构化标注,**降级不是静默的**,渲染层对 degraded 结果显式追加链路说明)、无降级时报错附平台升级指引;版本探测自身失败 fail-open 直通(守卫是路由优化不是安全边界)。版本比较采用数值分段(杜绝 `'2.96.0' > '2.100.0'` 字典序陷阱),探测结果 30s TTL 缓存且只缓存成功值;
109
+ - **gh 版本守卫降级链(runGuarded)**:preview 命令与新特性按 `minVersion` 门槛路由——版本达标直通主命令、不足走 `fallback` 降级链路(结果携带 `degraded/degradedReason` 结构化标注,**降级不是静默的**,渲染层对 degraded 结果显式追加链路说明)、无降级时报错附平台升级指引;版本探测自身失败 fail-open 直通(守卫是路由优化不是安全边界)。版本比较采用数值分段(`src/executor/version-utils.ts`,杜绝 `'2.96.0' > '2.100.0'` 字典序陷阱),探测结果 30s TTL 缓存且只缓存成功值;
100
110
  - **AI_AGENT 识别信号注入**:每次 gh 子进程启动注入 `AI_AGENT=dsh`(gh detect.go 最高优先级通用信号)——gh ≥2.99 自动切换 text-only spinner、≥2.100 命令误用时打印完整 help(模型可据此自我纠正)、telemetry 归因到 dsh;旧版 gh 忽略该变量零副作用。经 `agentEnvInjection` 配置开关(默认开启)可恢复 gh 原生行为。
101
111
 
102
112
  ### 2.3.1 高危操作确认与敏感值安全通道 (Safety Confirmation & Secret stdin Channel)
103
- - 不可逆写操作(`merge` / `close` / `issue delete`(物理删除)/ `issue transfer`(跨仓库迁移)/ `pr revert`(远端创建反向 PR)/ `release delete` / clobber 覆盖上传 / `secret delete` / `skill install`)强制要求 `confirm: true` 显式参数,未确认时仅返回影响评估卡片,不产生任何远端或本地副作用;
104
- - 密钥明文仅经子进程标准输入传递(`spawnWithStdin` 通道:`spawn` + `stdin.end(data)` 半关闭写入——`execFile` 的 `input` 选项在实测环境从不真正写入管道,密文通道此前从未生效过),全程不出现在命令行参数、进程列表与工具输出中;渲染层对 secret 输出采用独立实现,从结构上隔离「绝不渲染 value 字段」这条安全红线。secret 与 variable(CI 明文,get 可回显)的渲染走**两个物理独立的函数**——安全语义靠函数边界而非同一函数的分支隔离,防未来重构互相污染。
113
+ - 不可逆写操作(`merge` / `close` / `issue delete`(物理删除)/ `issue transfer`(跨仓库迁移)/ `pr revert`(远端创建反向 PR)/ `release delete` / clobber 覆盖上传 / `secret delete` / `skill install` / `label delete`(物理删除且解绑)/ `cache delete all`(全量清空缓存)/ `gist delete`(代码片段永久删除))强制要求 `confirm: true` 显式参数,未确认时仅返回影响评估卡片,不产生任何远端或本地副作用;
114
+ - 密钥明文仅经子进程标准输入传递(`spawnWithStdin` 通道:`spawn` + `stdin.end(data)` 半关闭写入),全程不出现在命令行参数、进程列表与工具输出中;渲染层对 secret 输出采用独立实现,从结构上隔离「绝不渲染 value 字段」这条安全红线。
105
115
 
106
116
  ### 2.3.2 产物落盘纪律 (Artifact Download Discipline)
107
- - **目录固定派生,永不暴露路径参数**:构建产物下载(`run download` 等)的落盘目录由插件从 `~/.dsh/data/dsh-github-flow/artifacts/<owner>/<repo>/<run_id>/` 三级固定派生,不向调用方暴露任何目录参数——这是 GHSA-39wj-f2f4-978v(gh <2.102 符号链接路径穿越)的核心防线:用户无法注入 `../` 或符号链接目标,攻击面收敛到唯一一个 `mkdirSync(recursive)` 调用点,且目录天然按仓库与 run 隔离、可整体清理;
108
- - **低版本安全提示而非硬拒绝**:gh <2.102 时下载仍可执行(受控仓库场景是合理需求),但返回值携带 `securityNote` 升级指引;需要自定义落盘位置的场景显式引导经 `github_api` 手动拉取;
117
+ - **目录固定派生,永不暴露路径参数**:构建产物下载(`run download` 等)的落盘目录由插件从 `~/.dsh/data/dsh-github-flow/artifacts/<owner>/<repo>/<run_id>/` 三级固定派生,不向调用方暴露任何目录参数——这是 GHSA-39wj-f2f4-978v(gh <2.102 符号链接路径穿越)的核心防线;
118
+ - **低版本安全提示而非硬拒绝**:gh <2.102 时下载仍可执行,但返回值携带 `securityNote` 升级指引;需要自定义落盘位置的场景显式引导经 `github_api` 手动拉取;
109
119
  - **下载后零本地符号化操作**:插件对落盘产物仅做递归枚举(相对路径 + 大小清单),不执行链接、移动或执行类操作。
110
- 未来任何新增下载类能力(如 gist 资产)必须引用本节纪律,而非各自重述。
111
-
112
- ### 2.4 原生浅色/深色主题自适应与快捷操作网格
113
- - 全面对齐 DSH 官方 DSW 语义设计变量,基于 `body[data-ds-dark-theme]` 驱动无闪烁双模式切换;
114
- - 右侧栏快捷操作栏彻底废弃失效的阻塞弹窗,全面采用官方直达模式(查看所有 Issues、查看 PRs、Compare / New Issue),享有 100% 官方富文本体验。
115
-
116
- ### 2.5 Cordis 微内核标准规范
117
- - **按需依赖声明**:显式声明核心必需服务 `['tools', 'commands']`,防止因无头运行环境缺少 `webServer` 或 `workspaceRegistry` 导致插件陷入 `PENDING` 假死状态;
118
- - **优雅动态探测**:通过 `if (ctx.webServer)` 探测环境,有 Web UI 服务时动态挂载路由,无 Web 环境时无缝降级为纯 CLI 工具;
119
- - **生命周期 Effect 闭环**:所有 HTTP 路由与命令注册均持有 Disposer 清理函数,插件热重载或重新挂载时自动彻底注销,零内存泄漏;
120
- - **配置自校验与服务暴露**:基于 Standard Schema 规范实现强类型校验与默认值注入,通过 `ctx.provide('github', executor)` 向生态插件暴露单例服务。
121
-
122
- ### 2.6 三态环境诊断与交互引导状态机 (Setup & Authentication Lifecycle Guard)
123
- - **三态状态模型 (Three-State Machine)**:将环境就绪判定严格划分为 `NOT_INSTALLED`(未安装 CLI)、`NOT_LOGGED_IN`(已安装未登录)、`AUTHENTICATED`(已认证)三态,彻底消除“把未安装当作未登录”的状态屏蔽隐患;
124
- - **平台自适应安装策略**:后端探测到 `ENOENT` 时根据 `process.platform` 组装对应平台的安装命令(Windows: `winget`,macOS: `brew`,Linux: `apt/dnf`)与官方安装包地址;
125
- - **安全辅助与白名单终端唤起**:Web 端提供一键命令复制,后端 `/api/github/action` 提供安全白名单唤起接口,避免在前端模拟复杂交互式 PTY 的脆弱性;
126
- - **焦点感知自愈 (Focus Auto-Recovery)**:前端挂载 `window.focus` 事件监听,用户在外部浏览器或终端完成认证后切回 DSH 界面,自动静默重新探测并无缝更新 UI,实现零摩擦闭环体验。
127
-
128
- ### 2.7 多维 Issue 身份判定与并发聚合引擎 (Multi-Dimensional Issue Engine)
129
- - **多重身份决策矩阵**:基于当前登录用户 `currentUser`(`auth.user`)对 Issue 执行身份归类:
130
- - `reported`(用户提单):`repoOwner === currentUser && author != currentUser`(外部访客在本人维护的仓库中提出的反馈);
131
- - `assigned`(分配待办):`assignees.includes(currentUser)`(指派给本人的任务,跨仓库通用);
132
- - `created`(我发起的):`author === currentUser`(本人亲自创建的 Issue)。
133
- - **三路并发搜索与 URL 去重**:后端在 `/api/github/global-overview` 中通过 `Promise.all` 同时发起 `--owner=@me`、`--assignee=@me`、`--author=@me` 检索,按 URL 唯一性合并去重并按最后更新时间统一降序排列;
134
- - **双端视图无缝打标**:驾驶舱提供四态响应式药丸筛选(全部/用户提单/指派待办/我发起的),右侧栏直接常驻展现当前工作区所属 Issue 任务清单与状态徽章。
135
-
136
- ### 2.8 智能版本检测与跨平台一键升级引擎 (Update & Upgrade Engine)
137
- - **检查与 gh 链路完全并行**:
138
- - 前端版本检查与 gh 环境探测(安装/登录/业务数据)**零依赖并行发起**(挂载即触发,仅 300ms 微延迟避让首屏),不串行等待概览返回,gh 环境异常时升级横幅照常展示;
139
- - 服务端 `inFlightPromise` 请求合流 + 24 小时缓存,多面板同时挂载零请求风暴。
140
- - **更新源三态可选(UI 持久化 store 优先)**:
141
- - 三态单选:官方源(默认)/ 国内镜像 npmmirror / 关闭自动检查;
142
- - 持久化于 `~/.dsh/data/dsh-github-flow/update-settings.json`,保存即热切换并失效 24h 缓存(防"切换无效"感知);
143
- - **单源严格不互备**:显式选择的源不静默回落——镜像同步滞后正是选官方的理由,探测失败如实返回;
144
- - 「关闭检查」语义为关自动提示不关能力:`/gh upgrade`(force)与一键升级路由不受熔断;
145
- - 一键升级的 `NPM_CONFIG_REGISTRY` 跟随所选源(注入查询函数而非静态值,切源实时生效)。
146
- - **跨平台 Shell 调度与 macOS PATH 自愈**:
147
- - **Windows**:采用 `cmd.exe /d /s /c`,彻底规避 PowerShell 5.1 不支持 `||` 导致的语法崩溃,启动仅 ~10ms;
148
- - **macOS / Linux**:采用用户 Default Shell(`/bin/zsh` 或 `/bin/bash`);
149
- - **macOS PATH 智能补全**:针对 Mac 桌面端由 GUI (Finder/Dock) 启动不继承 `.zshrc` 的痛点,自动将当前 Node 目录、Apple Silicon `/opt/homebrew/bin` 及 Intel `/usr/local/bin` 动态注入 PATH 前端,确保 100% 发现 npm/pnpm。
150
- - **环境自省与源码保护防线 (Dev-Link Safe)**:
151
- - 自动识别当前是否处于本地 Git 源码软链接模式(`link:`),若是则自动阻止 npm 覆盖,保护工作区源码;
152
- - 自动识别桌面版 Profile 目录,无全局 CLI 时直接在 `~/.dsh/profiles/desktop` 下就地更新,无需用户具备终端知识。
153
- - **并发互斥锁与重启提示闭环**:
154
- - 内存互斥锁防止连续点击导致包管理器 lockfile 竞争;
155
- - 升级完成后明确提醒用户重启 DSH 服务以完全加载最新后端模块,同时支持立即刷新页面预览新界面。
156
-
157
- ### 2.9 客户端持久化与 SWR (Stale-While-Revalidate) 极速直出架构
158
- - **0ms 首屏瞬间直出机制**:
159
- - 在全景驾驶舱组件挂载时,`data` 初始值直接通过 `React.useMemo` 从 `localStorage` 读取上一次保存的历史快照;
160
- - 消除传统加载时全屏阻塞卡顿,彻底抹平每次打开等待 8~15 秒的痛点,达到即开即用的丝滑体感。
161
- - **透明数据新鲜度与状态胶囊 (SyncStatusBadge)**:
162
- - 胶囊状态精准映射:`[⏳ 后台同步中...]`、`[✅ 已是最新 (15:30)]`、`[📦 本地快照 (X分钟前)]`、`[⚠️ 同步超时已保留缓存]`;
163
- - 支持人性化相对时间推导(刚刚、X分钟前、X小时前、X天前),消除用户对数据时效性的疑虑。
164
- - **设置中心 (GlobalSyncSettingsModal) 四分区**:
165
- - **🔄 数据同步**:`⚡ 秒开模式 (SWR, 默认)` / `📋 仅手动刷新 (Manual Only)` 两态选择;
166
- - **🌐 网络代理**:代理配置即时全局生效(写 proxy.json + 重应用 dispatcher,零插件重载);
167
- - **⬆️ 更新设置**:更新源三态选择(官方/镜像/关闭),保存即热切换并真实验证;
168
- - **ℹ️ 关于**:插件身份信息(版本/仓库/npm/协议/求 Star);
169
- - 弹窗支持 `initialSection` 直开指定分区——插件徽标点击直开「关于」,齿轮默认「数据同步」。
170
- - **插件身份标识闭环**:
171
- - 面板标题(驾驶舱/右侧栏)带「插件」徽标,明确标注界面由第三方插件 `dsh-github-flow` 提供(非 DSH / GitHub 官方);
172
- - 徽标点击派发全局事件打开「关于」弹窗(不直接跳浏览器);多宿主同挂载时经可见性守卫防双层弹窗叠层;
173
- - 右侧栏同样提供 ⚙️ 设置按钮,与主页复用同一弹窗组件,保存经事件总线双侧实时对齐。
174
- - **离线极端容灾保护 (Fail-Safe Offline Guard)**:
175
- - 后台静默重验若遭遇断网、弱网或进程超时,界面平滑降级,**100% 保持旧数据完整可用**,绝不回退至空白占位或报错中断。
176
- - **数据链路超时防线**:
177
- - 全部前端数据链路 fetch 绑定 `AbortController`(45s 兜底;登录探测 10s)——代理劫持回环地址等极端场景下用户得到明确超时出口,绝不永久转圈(未安装 gh 用户的刷新卡死修复);
178
- - 后端 `gh` 探测毫秒级返回 ENOENT/安装引导,前端安装引导卡(AuthGuideCard)承载唤起终端(stdio ignore,无 profile)等自愈出口。
179
-
180
- ### 2.10 右侧栏五 Tab 互斥与本地 Git 状态全景架构 (Right Sidebar Tabs & Local Git Engine)
181
- - **五 Tab 响应式互斥布局**:
182
- - 将右侧栏原本纵向堆叠的 PR / Issues / Actions / Git 状态与未提交清单彻底重构成水平紧凑 Tab(Git / 未提交 / PR / Issues / Actions),默认激活 Git 记录页,首屏高度收敛 60% 以上;
183
- - 纯函数渲染约束与 DOM 级状态:`renderGithubRepoView` 坚守零 React Hooks 约束,激活态通过 `data-active-tab` 挂载在 DOM 容器上;根组件通过 `useEffect([data])` 与 `applyTabState` 联动,在 SWR 后台拉新完成触发重渲染时,自动重置回用户选择的 Tab,彻底解决后台数据刷新拽回默认 Tab 的缺陷。
184
- - **三段式提交日志与本地 0ms 秒开**:
185
- - 本地领先(待 push)、远程领先(待 pull,快照口径)与最近提交历史;
186
- - 采用 ASCII 单元/记录控制符(`%x1e` / `%x1f`)结构化解析 `git log`,一次性提取 fullSha、author、email、date、多行 body 说明与 `--stat` 文件改动统计,首屏 0ms 纯本地呈现,零网络失败风险;
187
- - 默认展示 15 条,通过右侧栏容器 `onScroll` 触底(<80px)与点击双重触发向后分页拉取,伴随实时进度计数(如 `15 / 74`)。
188
- - **内嵌模态弹窗 (CommitDetailModal)**:
189
- - 复用设置弹窗架构(`.dsh-github-modal-overlay` + `.dsh-github-modal-dialog`),挂载在右侧栏容器内部,天然受限于右侧栏边界居中,彻底消除传统 `<dialog>` 顶层穿透全屏居中的越界问题;
190
- - 弹窗外框限制 `max-height: 84vh; overflow: hidden;`,正文区域 `flex: 1; min-height: 0; overflow-y: auto;`,完美支持上下滑动浏览数十行详细正文与修改文件统计,支持 Esc 键与点击遮罩随手退出。
191
- - **全库统一 Git 官方矢量图标**:全量替换各处的植物/字符符号为标准 Git 菱形分支官方矢量组件(`#1296db` SVG)。
192
120
 
121
+ ---
122
+
123
+ ## 3. 工程解耦与单文件瘦身规范 (Modularization Specs)
124
+
125
+ 为彻底解决单文件臃肿过长(如旧版 `client.ts` 超过 5,200 行、`executor.ts` 超过 1,100 行)的技术债,项目已全面落地分层解耦与自包含构建规范:
126
+
127
+ ### 3.1 后端执行引擎分层 (`src/executor/`)
128
+ - [src/executor.ts](../src/executor.ts)(瘦身至 ~820 行):保留 `GhExecutor` 类核心编排(命令白名单、执行通道、认证与版本缓存),完整 Re-export 子模块符号,保持 100% 向后兼容;
129
+ - [src/executor/git-parser.ts](../src/executor/git-parser.ts):纯文本解析器(`status -sb`、`log`、`branch -a`),无 I/O、无副作用;
130
+ - [src/executor/version-utils.ts](../src/executor/version-utils.ts):语义化版本数值分段比对纯算法;
131
+ - [src/executor/process-utils.ts](../src/executor/process-utils.ts):`spawnWithStdin` 进程管道流控与 UTF-8 字符边界截断算法;
132
+ - [src/executor/platform-guide.ts](../src/executor/platform-guide.ts):跨平台 CLI 安装引导与升级命令文本生成。
133
+
134
+ ### 3.2 渲染层领域化细分 (`src/renderers/`)
135
+ - [src/renderers/markdown.ts](../src/renderers/markdown.ts)(瘦身至 14 行):门面出口,统一聚合导出全部领域子模块;
136
+ - [src/renderers/common.ts](../src/renderers/common.ts):表格单元格字符净化、各类状态药丸 Emoji 徽章、API 降级透明注解;
137
+ - `pr.ts` / `issue.ts` / `run.ts` / `repo.ts` / `release.ts` / `skill.ts`:6 大领域独立渲染格式化,避免跨工具改动合并冲突。
138
+
139
+ ### 3.3 本地 Git 元数据监听服务 (`src/git-watcher.ts`)
140
+ - 独立底层服务模块,彻底剥离路由控制器内部的物理文件系统监听与定时器杂糅;
141
+ - 靶向非递归监听 `.git/HEAD` 与 `.git/index`,内置 800ms 防抖合并、35s 超时熔断与物理句柄安全释放;
142
+ - 使 [src/api/routes-actions.ts](../src/api/routes-actions.ts) 彻底移除对 `node:fs` 与 `node:path` 的直接依赖,路由函数回归纯粹的 HTTP 请求接收与响应。
143
+
144
+ ### 3.4 前端客户端解耦与自包含 Bundle 机制 (`src/ui/` + `src/client.ts`)
145
+ - **源码物理剥离**:
146
+ - [src/client.ts](../src/client.ts)(由 5,290 行直降至 **1,263 行,瘦身 76%**):仅保留右侧栏核心协同 `renderGithubRepoView` 与 `GithubRightbarBody` 及插件入口;
147
+ - 12 个独立子模块下沉至 [src/ui/](../src/ui/) 目录(样式 1,295 行、全景大面板 732 行、设置弹窗 561 行、业务卡片 60~290 行);
148
+ - **命名空间防护**:
149
+ - 严禁在与 `package.json` 的 `exports["./client"]` 同级创建名为 `client/` 的目录,统一命名为 `src/ui/` 与编译产物 `lib/ui/`,彻底杜绝模块解析器产生“同名文件 vs 目录”的歧义报错;
150
+ - **自包含 Bundle 聚合器 (`scripts/build.mjs`)**:
151
+ - 构建脚本按严格拓扑顺序将 `lib/ui/` 子模块内联注入 `lib/client.js` 的 `window.__ModuleLoader__.load` 闭包中,移除 `export` 关键字;
152
+ - 保证 `GithubRightbarBody` 位于 `GithubWorkspacePanel` 之前(满足切片测试断言);
153
+ - 对嵌入的 1,290 行纯 CSS 实施紧凑压缩,消除空行与多余注释,使编译产物从 5,282 行减至 4,008 行;
154
+ - **React Hook 时序安全防线**:
155
+ - 组件内引用的 `useRef`(如 `containerRef`)必须在组件声明最前列初始化,严禁跨行下置引发变量提升 `undefined`;所有的 Ref 访问点必须配备 `containerRef && containerRef.current` 守卫,杜绝运行时渲染崩溃。
156
+
157
+ ---
158
+
159
+ ## 4. 自动化测试体系 (Test Suite)
160
+
161
+ 项目配备了 13 个独立阶段的全量自动化回归测试套件(`test/run-all.mjs`),覆盖 **562 项全量自动化测试断言**:
162
+ - `phase1`:Issue / Run / PR / Repo / Help 命令装配与确认门禁;
163
+ - `phase2`:Native Markdown 渲染与 Token 压缩率实测(>60% 降幅);
164
+ - `phase3`:Release 资产交付、Secret 密文管道与 Skills 检索;
165
+ - `phase4`:工具装配完整性、版本一致性拦截与模糊边界防护;
166
+ - `phase5`:网络代理配置有效性校验、环境注入与容灾降级;
167
+ - `phase6`:插件版本更新检测、多源选择与跨平台自升级调度;
168
+ - `phase7`:终端唤起真实反馈、更新源设置 UI 锚点与 45s 数据链路超时保护;
169
+ - `phase8`:图片链接提取与图片下载;
170
+ - `phase9`:README 提取与相对路径解析;
171
+ - `phase10`:版本守卫降级链(runGuarded)、AI_AGENT 注入与命令白名单;
172
+ - `phase11`:右侧栏五 Tab 响应式互斥、Git 提交记录与滑动分页;
173
+ - `phase12`:工作区物理所有权隔离、SWR 离线快照防抹杀与安全分支切换;
174
+ - `phase13`:云端定时自动同步生命周期、三选一互斥设置模型与右侧栏独占本地 git-watch 监听。
@@ -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,21 @@ 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
+ }
229
246
  default:
230
247
  sendJson(res, 400, { ok: false, message: `未知的操作类型: ${payload.action}` });
231
248
  }
@@ -236,4 +253,104 @@ export function registerActionRoutes(deps, executor, cacheMap, globalCache) {
236
253
  });
237
254
  },
238
255
  });
256
+ // 3. 读取当前工作区的所有 Git 分支列表(纯本地只读,毫秒级响应)
257
+ bindRoute({
258
+ kind: 'exact',
259
+ path: '/api/github/branches',
260
+ handler: async (req, res) => {
261
+ if (req.method !== 'GET') {
262
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
263
+ return;
264
+ }
265
+ try {
266
+ const url = new URL(req.url || '', 'http://127.0.0.1');
267
+ const cwd = url.searchParams.get('cwd') || undefined;
268
+ const result = await executor.getGitBranches(cwd);
269
+ if (!result) {
270
+ sendJson(res, 404, { ok: false, error: '当前工作区不是 Git 仓库或无法读取分支' });
271
+ return;
272
+ }
273
+ sendJson(res, 200, { ok: true, data: result });
274
+ }
275
+ catch (err) {
276
+ sendJson(res, 500, { ok: false, error: err?.message || '读取分支列表失败' });
277
+ }
278
+ },
279
+ });
280
+ // 4. 切换当前工作区的 Git 分支(带未提交变更防护与合法性校验)
281
+ bindRoute({
282
+ kind: 'exact',
283
+ path: '/api/github/branch-switch',
284
+ handler: async (req, res) => {
285
+ if (req.method !== 'POST') {
286
+ sendJson(res, 405, { ok: false, message: '仅支持 POST 请求' });
287
+ return;
288
+ }
289
+ let body = '';
290
+ req.on('data', (chunk) => { body += chunk; });
291
+ req.on('end', async () => {
292
+ try {
293
+ const payload = body ? JSON.parse(body) : {};
294
+ const cwd = payload.cwd || undefined;
295
+ const branch = typeof payload.branch === 'string' ? payload.branch.trim() : '';
296
+ if (!branch) {
297
+ sendJson(res, 400, { ok: false, error: '缺少目标分支名称' });
298
+ return;
299
+ }
300
+ const r = await executor.switchGitBranch(cwd, branch);
301
+ if (!r.ok) {
302
+ sendJson(res, 400, { ok: false, error: r.error });
303
+ return;
304
+ }
305
+ // 核心联动:切换分支成功后,当前工作区缓存立即失效,确保后续拉取新分支数据
306
+ cacheMap.clear();
307
+ sendJson(res, 200, {
308
+ ok: true,
309
+ currentBranch: r.currentBranch,
310
+ message: `已成功切换到分支 ${r.currentBranch}`,
311
+ });
312
+ }
313
+ catch (err) {
314
+ sendJson(res, 500, { ok: false, error: err?.message || '切换分支失败' });
315
+ }
316
+ });
317
+ },
318
+ });
319
+ // 5. 本地 Git 元数据定向监听(专供右侧栏开启「本地 Git 改动实时感知」时长轮询)
320
+ // 严格安全纪律:只针对 .git/index 与 .git/HEAD 实施定向非递归监听,严禁监听源码目录防句柄耗尽;
321
+ // 800ms 防抖合并;连接断开时自动关闭 watcher 释放系统资源(委托给底层 git-watcher 服务)。
322
+ bindRoute({
323
+ kind: 'exact',
324
+ path: '/api/github/git-watch',
325
+ handler: async (req, res) => {
326
+ if (req.method !== 'GET') {
327
+ sendJson(res, 405, { ok: false, message: '仅支持 GET 请求' });
328
+ return;
329
+ }
330
+ const url = new URL(req.url || '', 'http://127.0.0.1');
331
+ const cwd = url.searchParams.get('cwd');
332
+ if (!cwd || !hasGitDir(cwd)) {
333
+ sendJson(res, 400, { ok: false, error: '非 Git 仓库,无需监听' });
334
+ return;
335
+ }
336
+ let responded = false;
337
+ try {
338
+ const stop = watchGitMetadata(cwd, (event) => {
339
+ if (responded)
340
+ return;
341
+ responded = true;
342
+ sendJson(res, 200, { ok: true, ...event });
343
+ });
344
+ req.on('close', () => {
345
+ if (!responded) {
346
+ responded = true;
347
+ stop();
348
+ }
349
+ });
350
+ }
351
+ catch (err) {
352
+ sendJson(res, 500, { ok: false, error: err?.message || '建立监听失败' });
353
+ }
354
+ },
355
+ });
239
356
  }
@@ -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
  }