@marktrue/dsh-mobile-remote 3.2.3-rc.1 → 3.2.4-alpha.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.
package/CHANGELOG.md CHANGED
@@ -1,8 +1,28 @@
1
1
  # Changelog
2
2
 
3
- ## v3.2.3-rc.1(build 28,issue #39 + 上游 v3.2.3)— NPM-only 自动更新 + 上游 v3.2.3 并入
3
+ ## v3.2.4-alpha.1(build 33,issue #42)— Git 浏览增强:全量标签 / 结构化 refs / 统一文件查看器 / 文件提交历史
4
4
 
5
- > 插件 `3.2.3-rc.1` / App `3.2.3+28` / 载体 `3.2.3-rc.1-app`。本 fork 的 RC 发布候选;插件发布到 npm `rc` dist-tag,App build 从 +27 提升到 +28。
5
+ > 插件 `3.2.4-alpha.1` / App `3.2.4+33` / 载体 `3.2.4-alpha.1-app`。develop 快照:合并 issue #42 上游贡献分支(`upstream/git-browser-upgrade`,已 rebase 到 `github/main`),并带入 main 最新内容(宠物资源瘦身、审批默认 1 小时等)。
6
+ >
7
+ > 2026-11-03 重新打 tag:build 32 的 `v3.2.4-alpha.1` 未发布即删除,本次使用全局单调 build 33。
8
+
9
+ ### 插件:Git 只读浏览契约 1.1.0(issue #42)— 结构化 refs/tags + 文件列表与文件历史
10
+
11
+ - `GET /m/api/git/capabilities`:`contractVersion` 升至 `1.1.0`;`features` 新增 `refsDetail` 与 `fileHistory`(两项独立降级,旧 `1.0.0` 外部 provider 继续兼容)。`fileHistory` 仅在**当前生效的** provider 实现 `files()` / `fileHistory()` 时为 `true`,不会用包内 bundled 数据补齐外部 provider 的视图。
12
+ - `GET /m/api/git/graph`、`GET /m/api/git/commit`:保留旧 `refs: string[]`,新增 `refsDetail` / `tagsDetail`(HEAD → 本地分支 → 远程分支 → tag 排序,排除 `refs/stash`,commit 详情返回 annotated tag 的 tagger/date/subject,subject ≤ 200 字);旧 provider 的原始 refs 字符串由宿主解析兜底,`for-each-ref` 读取改为 NUL/US 分隔(tagger/tag subject 中的 tab 不再破坏解析)。
13
+ - 新增 `GET /m/api/git/files`(HEAD 已跟踪文件,大小写不敏感 `q` 子串过滤,`limit` ≤ 200)与 `GET /m/api/git/file-history`(单文件提交历史,默认 20 / 最多 100,不透明游标,5000 条 / 20 秒上限;空/异常结果以 200 + `reason` 返回),路径由宿主归一化并拒绝越界。
14
+ - 文档:`docs/03-api.md` 新增自包含的 Git 只读浏览接口章节(§6.19)。
15
+
16
+ ### App:统一 Git 文件查看器与文件提交历史
17
+
18
+ - 新增 `GitFileViewer`:`content` / `diff` 统一渲染(行号、Markdown 预览/源码切换、diff 折叠、横向滚动、复制),会话文件浏览与 git diff 预览复用同一组件。
19
+ - 分支图展示全部 refs/tags:单行共享横向滚动,不再 `+N`/截断;按 HEAD → 本地分支 → 远程分支 → tag 排序,颜色按种类区分;提交详情 Refs/Tags 展示结构化数据与 annotated tag 元数据(旧宿主字符串兜底)。
20
+ - Git 管理新增可选「文件提交历史」tab:候选 4 个仍限选 1–3(默认顺序不变);顶部文件选择器(仓库内搜索 + 最近文件)、分页历史(20/页)、文件状态徽标、长按复制 OID;查看器新增「提交历史」入口,tab 未启用时打开独立历史页。
21
+ - 旧宿主/旧 provider 全链路降级:`fileHistory` 能力缺失隐藏入口,旧 provider 的原始 refs 字符串由 App 解析兜底。
22
+
23
+ ## v3.2.3-alpha.1(build 27,issue #39)— NPM-only 自动更新(develop 收口)
24
+
25
+ > 插件 `3.2.3-alpha.1` / App `3.2.3+27` / 载体 `3.2.3-alpha.1-app`。develop 已合并 issue #39 实现;本条目为收口后的开发快照。
6
26
 
7
27
  ### 更新源与安全
8
28
 
@@ -25,29 +45,28 @@
25
45
 
26
46
  - Windows:npm/pnpm 经 shell 调用,缺失客户端给出稳定原因;桌面原生自更新仍受 `restart-unsupported` 限制(Linux/WSL 宿主可完成自更新)。
27
47
 
48
+ ## v3.2.3(2026-11-01,build 31 重发,issue #55 / PR #56)— 后台提醒保活 + 通知判据与精确撤销
28
49
 
29
- ### 上游原文(main v3.2.2 / v3.2.3,截至 `a4fb7a3`)
30
-
31
- 以下为上游 main 已发布条目的原文(标题层级已下移以适配本节),按 main 顺序保留:
32
-
33
- #### v3.2.3(2026-10-31,build 30 重发,issue #55 / PR #56)— 后台提醒保活 + 通知判据与精确撤销
50
+ > 版本:插件 `3.2.3` / App `3.2.3+31`。对 v3.2.3(build 30)的**同版本号重发**,Release 资产替换;构建号 30 → 31。
51
+ > 验收:`flutter analyze` 0 issue、`flutter test` 593、`node --test test/*.mjs`(审批相关单跑 13/13)、真机见下。
34
52
 
35
- > 版本:插件 `3.2.3` / App `3.2.3+30`。对 v3.2.3(build 29)的**同版本号重发**,Release 资产替换;构建号 29 → 30。
36
- > 验收:`flutter analyze` 0 issue、`flutter test` 593、`node --test test/*.mjs`(审批相关单跑 13/13)、真机 9 项(见下)。
53
+ ### 真机验收(Xiaomi 17 Pro Max / Android 16,2026-11-01)
37
54
 
38
- ##### 真机验收(build 29 的实现,Xiaomi 17 Pro Max / Android 16,2026-10-31)
39
-
40
- - 常驻通知模式启动:`KeepAliveService` 前台运行、通道 `dsh_keep_alive`、通知 ID 1041,`ONGOING|ONLY_ALERT_ONCE|NO_CLEAR`。
41
- - 与桌面宠物互斥:切常驻通知 → 宠物服务停止;切回桌面宠物 → 常驻服务停止。
42
- - 选「关闭」→ 两个服务都停。
43
- - `force-stop` 后重开 App → 按持久化模式自动恢复常驻服务。
44
- - 按 HOME 退到后台 190 秒:进程 PID 与 ServiceRecord 不变,服务存活。
55
+ - 常驻通知启动:`KeepAliveService` 前台运行、通道 `dsh_keepalive_v2`、通知 ID 3515、`types=0x40000000`(specialUse),`ONGOING|ONLY_ALERT_ONCE|NO_CLEAR`。
56
+ - 状态行「保活运行 · 心跳 服务7/Dart5」:原生与 Dart 心跳都在走。
57
+ - 与桌面宠物双向互斥:切保活 → 宠物停;切宠物 → 保活停。
58
+ - 按 HOME 退后台 150 秒:PID 与 ServiceRecord 不变,服务存活。
59
+ - 开启保活后设置页出现「系统省电设置」入口。
45
60
  - 通知权限被拒时选「常驻通知」→ 弹「需要通知权限」且不启动服务。
46
61
  - 端到端:后台收到真实审批帧 → 系统通知出现(`dsh_attention`);`/respond` 结算后通知自动撤销。
47
62
 
48
- > build 30 并入 PR #56 后,原生保活服务与通知判据换成了其实现(通道 `dsh_keepalive_v2`、ID 3515、启动前 `resolveService` 校验),PR 作者在 MIUI / Android 14 上验过;本机 build 30 的复测待设备重新连上后补。
63
+ ### build 31 修复
64
+
65
+ - 切到常驻通知时同步持久化宠物开关(`floatingEnabled=false`),App 启动时的恢复守卫也检查保活偏好。
66
+ - 真正漏点:主框架用 `IndexedStack`,设置页随 App 启动就 `initState`,其 `_initBubbleState()` 按残留的旧偏好无条件拉起宠物——与保活开关无关。现在该处也做互斥判断,并把冲突的旧偏好纠正为 `false`。
67
+ - 现象(build 30 实测):先开宠物再切到保活,`force-stop` 重启后 `KeepAliveService` 正常恢复,但 `FloatingBubbleService` 也被拉起,两条常驻通知(同一通知 ID 3515)互相覆盖。
49
68
 
50
- ##### 通知判据与身份(PR #56,合并后以此实现为准)
69
+ ### 通知判据与身份(PR #56,合并后以此实现为准)
51
70
 
52
71
  - 新增 `lib/attention.dart`:把「要不要弹、弹哪条、怎么撤」抽成纯逻辑层(`AttentionEvent` / `decideAttention` / `AttentionLedger`),key = `kind|sessionId|approvalId 或 rpcId`,通知 id 用稳定哈希,跨重连与重启都不重复、不撤错。
53
72
  - 进会话即已读:打开会话时该会话未读自动标已读并撤掉对应横幅;横幅按会话合并,全局一条,点击直达。
@@ -56,7 +75,7 @@
56
75
  - 原生启动前用 `resolveService` 校验组件可解析——此前 manifest 漏声明时 `startForegroundService` 不抛异常,UI 会假成功。
57
76
  - 单测新增 `attention_test.dart`(38 条)与通知测试补充,总数 593。
58
77
 
59
- ##### 后台提醒:三选一模式(#55 步骤 4)
78
+ ### 后台提醒:三选一模式(#55 步骤 4)
60
79
 
61
80
  - 设置 → 后台提醒:`关闭 / 常驻通知 / 桌面宠物` 三选一,互斥(切换时先停另一个服务,不会出现两条常驻通知)。
62
81
  - 新增 `KeepAliveService`(`specialUse` 前台服务):一条低干扰常驻通知把进程钉住,让 Dart 侧 SSE 在后台不被冻结;不自己连 SSE,也不碰宠物渲染。
@@ -64,28 +83,42 @@
64
83
  - `onTaskRemoved → stopSelf()`:划掉最近任务时诚实退出,避免"通知还在、进程已死"的假在线。
65
84
  - 切到「桌面宠物」沿用原悬浮窗权限引导与失败提示。
66
85
 
67
- ##### 通知身份与撤销(#55 步骤 1)
86
+ ### 通知身份与撤销(#55 步骤 1)
68
87
 
69
88
  - 审批通知的 id 与去重键改用 `approvalId`(问题帧退回 `rpcId`):SSE 重连重放同一审批帧不再堆叠多条通知。
70
89
  - `approval/resolved` / `question/resolved` 到达时按 id 精确撤销对应系统通知。
71
90
  - 回到前台不再 `cancelAll()`——旧行为会把仍未处理的审批提醒一起抹掉。
72
91
  - 插件:`approval/resolved` 帧补 `sessionId` 与 `outcome`,`question/resolved` 补 `sessionId`。
73
92
 
74
- ##### 插件(#55 步骤 3)
93
+ ### 插件(#55 步骤 3)
75
94
 
76
- - `pendingTimeoutMs` 改为可配置(默认仍 120s),`offlineHoldMs` 保持原默认 300s。
95
+ - `pendingTimeoutMs` 改为可配置,`offlineHoldMs` 保持原默认 300s。
77
96
  - `hello` 与 `/api/bootstrap` 的 `capabilities` 增加 `hold: { onlineMs, offlineMs }`,手机端只用它展示剩余时间,结算仍以服务端为准。
78
97
 
79
- ##### 未包含(按 issue 约定后置)
98
+ ### 插件:审批默认保持 1 小时(PR #59,@qudongzhen)
80
99
 
81
- - 首次连接后的保活引导弹窗、进会话即已读与横幅合并(issue 步骤 2),下一版做。
100
+ - 默认在线保持从 120 秒放宽到 1 小时(`DEFAULT_PENDING_TIMEOUT_MS`),仍可用 `pendingTimeoutMs` 覆盖。手机上看代码再答,2 分钟经常不够。
101
+ - 超时结算从 `unavailable` 改为 `rejected`(手机超时、双端超时两处)——App 与桌面都显示「超时自动拒绝」,而不是「没有答复」。
102
+ - 离线回放 TTL 30 → 70 分钟,必须大于在线保持时长,否则回放帧会先于结算被清掉。
82
103
 
83
- #### v3.2.3(2026-10-30,issue #50 / #52,PR #51 / #53 / #54)— 工作区归属修复 + 大文件流式下载 + Markdown 预览
104
+ ### 宠物资源瘦身(issue #58)
105
+
106
+ - 两套动画重新编码,只改素材不动加载代码:总 23.7 MB → 8.3 MB。
107
+ - 原生侧 16 个 webp:288×229 / 121 帧 → 200×172 / 81 帧(12fps → 8fps),q63,单个 90–482 KB。
108
+ - App 内 4 个 webp:412×327 → 264×210,`idle` 81 帧 q58,点击类三个 61 帧 q60,单个 447–594 KB。
109
+ - 4 张静态 png 同步缩到 190 高(100 KB → 40 KB)。
110
+ - 未做(留待下一版):两套资源合并成一份、悬浮球通知链路职责收口(两者都要改加载/服务代码)。
111
+
112
+ ### 未包含(按 issue 约定后置)
113
+
114
+ - 首次连接后的保活引导弹窗(issue 步骤 2 的引导部分),下一版做。
115
+
116
+ ## v3.2.3(2026-10-30,issue #50 / #52,PR #51 / #53 / #54)— 工作区归属修复 + 大文件流式下载 + Markdown 预览
84
117
 
85
118
  > 版本:插件 `3.2.3` / App `3.2.3+28`。
86
119
  > 验收门禁:`node --test test/*.mjs`(208 通过 + 1 平台跳过,`interaction-settlement` 单跑 13/13)、`flutter analyze`(0 issue)、`flutter test`(555)、`flutter build apk --debug` 通过。
87
120
 
88
- ##### 建会话前确保 cwd 有归属工作区(#50)
121
+ ### 建会话前确保 cwd 有归属工作区(#50)
89
122
 
90
123
  - **问题**:App 目录选择器可选到任意目录(含新建文件夹),若该目录不在 `workspaceRegistry` 里,会话会带着「无归属工作区」的 `cwd` 落盘。内核 `WorkspaceEntity.attachSession()` 要求 `header.cwd` 与工作区根**完全相等**,于是:
91
124
  - 本次 attach 走「向上找祖先工作区」分支必然抛错,且原实现 `workspace?.attachSession(...)` **未 await**,异常逃过 `try/catch`;
@@ -95,7 +128,7 @@
95
128
  - **顺带**:attach 改为 `await` + `catch` 记 warn,避免未处理的 Promise rejection。
96
129
  - **新增回归**:`test/issue50-cwd-workspace.test.mjs`(未注册→注册并 attach;已注册→复用;目录不可用→400 且不建会话;旧宿主→回落且不改写 cwd)。
97
130
 
98
- ##### 插件:`/files` 支持 Range / HEAD / ETag(#52 / PR #53)
131
+ ### 插件:`/files` 支持 Range / HEAD / ETag(#52 / PR #53)
99
132
 
100
133
  - 单区间 `Range: bytes=...` → `206` + `Content-Range`;越界或格式错 → `416` + `bytes */size`(多区间仍不支持,避免响应放大)。
101
134
  - `ETag`(size + mtime)与 `Last-Modified`,`If-None-Match` 命中回 `304`。
@@ -103,14 +136,14 @@
103
136
  - `/directories` 增加 `fileEntries: [{name,size,mtimeMs,mime}]`,`files` 仍保持纯字符串数组,旧 App 不受影响。
104
137
  - `hello` 与 `/api/bootstrap` 的 `capabilities` 增加 `fileRange`;旧插件不声明时 App 自动退回整文件下载。
105
138
 
106
- ##### App:流式下载(#52 / PR #54)
139
+ ### App:流式下载(#52 / PR #54)
107
140
 
108
141
  - 下载改为 `StreamedResponse` → 临时文件:进度回调、可取消、`Range` 续传(旧插件返回 200 时自动从头写);取消/失败清理半成品。
109
142
  - 完成后由原生按路径保存到 `Download/DSH-Remote`(MediaStore `IS_PENDING`,I/O 不在主线程),完成弹窗给「查看 / 关闭」两个出口。
110
143
  - 失败文案人话化(401/403、404、5xx、断网、超时、中断、证书、保存失败各有表述),原始异常折叠进可复制的技术详情。
111
144
  - `MainActivity` 新增 `saveFileFromPath` / `openFile` / `shareFile` / `decodeText`;旧保存路径不再写死包名,改用 `packageName`。
112
145
 
113
- ##### App:预览编码识别 + Markdown 渲染(#52 / PR #54)
146
+ ### App:预览编码识别 + Markdown 渲染(#52 / PR #54)
114
147
 
115
148
  - 预览只取前 256 KiB:`fileRange` 可用时发 `Range`,否则读满主动断开。
116
149
  - 编码判定改为「BOM → 无 BOM UTF-16 双向试解打分 → 严格 UTF-8(容忍采样末尾被截断的多字节字符)→ GB18030」,GB18030 走原生 `Charset` 解码;修掉真 UTF-8 中文被误判成 GB18030 的整篇乱码。
@@ -118,63 +151,80 @@
118
151
  - 修 `isWithinRoot` 的分隔符判断(Windows 下 `root\sub` 曾被判成越界)。
119
152
  - 测试:`flutter test` 555(基线 509 + 新增 46:download_stream、download_error、session_files_logic、session_files_widget)。
120
153
 
121
- #### v3.2.2(2026-10-29,build 27 重发)— 外观:三套皮肤 + 8 款图标 + 宠物联动
154
+ ## v3.2.2(2026-10-29,build 27 重发)— 外观:三套皮肤 + 8 款图标 + 宠物联动
122
155
 
123
156
  > 版本:插件 `3.2.2` / App `3.2.2+27`。对已发布 v3.2.2(build 25)的**同版本号重发**,Release 资产替换;构建号 25 → 27(26 为本地产物,从未发布)。
124
157
  > 插件 `lib/` 无逻辑改动;重新打包只为修掉 `package.json` 的 UTF-8 BOM(带 BOM 的 tgz 安装会被 npm 拒绝)。
125
158
  > 验收:`flutter analyze` 0 issue、`flutter test` 515、`node --test test/*.mjs` 200 通过 + 1 平台跳过、`gradlew :app:compileDebugKotlin` 通过。
126
159
 
127
- ##### 外观:三套皮肤(默认仍是原 DSH)
160
+ ### 外观:三套皮肤(默认仍是原 DSH)
128
161
 
129
162
  - `设置 → 外观` 新增皮肤选择:**深潜夜航**(墨绿黑底 / 声呐环 / 冷光气泡 / 潮汐波浪 / 薄荷绿主色 + 深色字)、**海盐汽水**(奶油底 / 可乐蓝按钮 / 光束与气泡)、**鲸落星海**(靛蓝底 / 星尘星座 / 月盘 / 金色主色 + 深色字)。
130
163
  - 每套皮肤 × 明暗 = 8 组完整令牌,挂在 `ThemeExtension`(`DshPalette`)上;页面统一走 `DshColors.xxx(context)`,不再逐层传 skin。默认皮肤令牌与旧版逐值一致,老用户视觉不变。
131
164
  - 首页在这三套皮肤下换成大肥鱼 Hero:左侧一句问候(按皮肤切换文案),右侧固定 132dp 角色,下方动态背景(30fps 上限、不可见即停、减少动效时只留静态渐变)。
132
165
 
133
- ##### 外观:8 款 App 图标
166
+ ### 外观:8 款 App 图标
134
167
 
135
168
  - 新增 `深海头像 / 贝壳徽章 / 月夜潮线 / 奶油贴纸 / 鲸邮票 / 霓虹灯牌 / 鲸尾剪影`,加原图标共 8 款,`设置 → 外观 → App 图标` 四列网格选择。
136
169
  - 实现用 8 个 `activity-alias`(`PackageManager` 一次只启用一个),`MainActivity` 不再直接持有 `LAUNCHER`;默认 `IconDefault` 的图标与原 `ic_launcher` 逐像素一致(仅重编码差异),**默认外观不变**。
137
170
  - 选择持久化在 `dsh_mr_app_icon`,启动时 `syncIcon` 对账;原生侧已是目标 alias 时不重复切换,避免每次启动触发桌面刷新。
138
171
  - 已知限制:Android 13 主题图标(monochrome 层)与 `roundIcon` 未提供,下个版本补。
139
172
 
140
- ##### 首页角色互动
173
+ ### 首页角色互动
141
174
 
142
175
  - 点头部摸头、点身体戳一下、点尾部甩尾,各有对应动作与台词气泡(1.8 秒回落);长按打开迷你面板(运行中会话 / 最近通知 / 桌面宠物状态 / 用量入口)。
143
176
  - 角色区域外滑动不被拦截(`GestureDetector` 只吃自己那块);`减少动效` 或系统「移除动画」时退回静态帧。
144
177
 
145
- ##### 桌面宠物与 App 联动(行为变化)
178
+ ### 桌面宠物与 App 联动(行为变化)
146
179
 
147
180
  - **App 在前台时收起桌面宠物**(默认开,可关):`MainActivity.onStart/onStop` 把前后台状态告诉服务,宠物根视图置 `GONE`,同时收掉状态气泡与迷你面板。解决两件事——悬浮宠物与首页角色同屏重复;移动窗口盖住「+ 新建会话」等按钮时**吞掉触摸**。
148
181
  - 用 `onStart/onStop` 而非 `onResume/onPause`(权限弹窗不算离开);`onStop` 而非 `onDestroy`(进程被杀时后者不保证回调)。服务与 Activity 同进程,用静态标记而非 `startService` 广播,规避 Android 12+ 后台启动前台服务的限制。
149
182
  - 恢复时回到「贴边完全露出」再走 5 秒自动缩进,不改用户拖过的纵向位置。
150
183
 
151
- ##### 「减少动态」改为「减少动效」并挪位
184
+ ### 「减少动态」改为「减少动效」并挪位
152
185
 
153
186
  - 该开关实际同时管桌面宠物、首页角色与首页背景三处,原来只写在桌面宠物卡里、文案也只提宠物。现改名 **减少动效**,移入 `设置 → 外观`,副标题写明覆盖范围与「系统级移除动画始终优先」。
154
187
  - 行为不变:`MediaQuery.disableAnimations`(系统无障碍)优先级最高且不可被 App 覆盖,App 开关只能在此基础上再关。
155
188
 
156
- ##### 图标入口迁移提示
189
+ ### 图标入口迁移提示
157
190
 
158
191
  - 升级后首次启动弹一次说明:启动入口从 `com.dsh.remote.MainActivity` 换成 `com.dsh.remote.IconDefault`,**桌面上的旧快捷方式可能点不开一次**,删掉后从抽屉重新拖一个即可;此后切换图标不再出现。
159
192
  - 只在 Android 提示(其它平台不涉及 alias 迁移),结果记在 `dsh_mr_icon_migration_tip`,不会重复打扰。
160
193
 
161
- ##### 其它
194
+ ### 其它
162
195
 
163
196
  - 对话页发送按钮的硬编码白色改为 `DshColors.onBrand(context)`,薄荷/金色底上会变深色图标,符合对比度要求。
164
197
  - APK 体积:宠物动画(原生 14.1 MB)+ 首页动画(9.6 MB)。实测 release 包约 99 MB(上一版 71.8 MB),如需瘦身可把首页那 4 组 webp 降采样或改静态帧。
165
198
 
166
- #### v3.2.2(2026-10-27,issue #46 / #49,PR #42)— 在线本地通知 + 轮次导航 + 二维码组网地址优选 + 审批兜底修复
199
+ ## v3.2.2(2026-10-27,build 26)— 桌面宠物:可互动大肥鱼
200
+
201
+ > 版本:插件 `3.2.2` / App `3.2.2+26`。该构建**当时未单独发布**,随 build 27 一并对外(见上一条)。
202
+ > 素材:`QCYTSN/dsh-dafeiyu` 当前 16 组动画(MIT;上游 `PC2005-cloud/dsh-pet`),0.7x / 12fps 转码,共 14.1 MB。
203
+ > 验收:`flutter analyze` 0 issue、`flutter test` 509、`node --test test/*.mjs` 200 通过 + 1 平台跳过、Kotlin `PetAnimationModelTest` / `PetStateReducerTest` / `UsagePanelModelTest` 全绿;真机确认动画播放、状态切换、贴边缩进与面板。
204
+
205
+ ### 桌面宠物
206
+
207
+ - 移除圆形鲸鱼悬浮球,改为原生悬浮宠物(Android 8+;API 28+ 播 animated WebP,更低版本退化为静态首帧)。
208
+ - 沿用大肥鱼的动画状态机:IDLE / THINKING / WORKING(搜索、命令变体)/ WAITING / SUCCESS / ERROR / DISCONNECTED;完成/失败为短促 pulse,工具失败只短暂提示,不把任务判死。
209
+ - 可互动性:点头部摸头、点身体戳一下、点右侧尾巴、双击专属摸头;拖动播放悬空姿势,松手落地并吸附最近边缘;1.2 秒互动冷却。
210
+ - 贴边躲藏:5 秒无操作后只露 45% 画布,透明度 0.45 + 去饱和 + 轻度模糊;点露出部分滑出;完成/等待/出错自动探头,普通工作状态不打扰。
211
+ - 状态气泡:标题 + 当前任务/进度(会话标题、已完成步数),点气泡打开面板;未读通知角标沿用旧逻辑,贴边时角标跟随到可见侧。
212
+ - 旧迷你面板完整保留(运行中会话 / 最近通知 / 用量与额度 / 余额 / 打开 App / 去充值),新增当前状态行与「关闭宠物」入口。
213
+ - 设置页新增宠物大小(76 / 96 / 116dp,默认 96)、减少动效(build 27 起移入「外观」卡)、贴边自动躲藏;沿用旧悬浮球开关的持久化 key。
214
+ - 原生 WebP 不压缩打包。
215
+
216
+ ## v3.2.2(2026-10-27,issue #46 / #49,PR #42)— 在线本地通知 + 轮次导航 + 二维码组网地址优选 + 审批兜底修复
167
217
 
168
218
  > 版本:插件 `3.2.2` / App `3.2.2+25`。
169
219
  > 验收门禁:`node --test test/*.mjs`(200 通过 + 1 平台跳过)、`timeline-contract`(125)、`account-usage`(15)、`account-usage-adversarial`(19)、`flutter analyze`(0 issue)、`flutter test`(509)。
170
220
 
171
- ##### 审批兜底:下游 unavailable 不再抢答(真机验收发现)
221
+ ### 审批兜底:下游 unavailable 不再抢答(真机验收发现)
172
222
 
173
223
  - `dualRoute` 原来把「下游返回 `unavailable`」也当成先答:没有任何桌面 `$events` 客户端时,内核转发立刻给 `unavailable`,插件会同时收掉手机侧卡片——手机根本来不及作答。
174
224
  - 现在只在桌面给出**真实答复**时才收手机卡;下游是 `unavailable` 时保留手机条目,等 `/respond` 或手机侧 fail-close 超时给出最终结果。
175
225
  - 新增回归:下游立即 `unavailable`,手机仍可作答且结果以手机为准;把修复回退该用例即变红。
176
226
 
177
- ##### 对话轮次导航(PR #42)
227
+ ### 对话轮次导航(PR #42)
178
228
 
179
229
  - **一期**:对话页右侧刻度轨按轮次跳转;长会话里的定位修复(尺寸缓存、有符号估算、尝试次数上界、尾部安全网)。
180
230
  - **二期**:接入宿主 `turnOutline` 投影,已加载轮次正常显示、未加载轮次以短淡刻度显示,点它按 **200 条/页**有界跨页加载后定位;用户主动滚动即取消,四种终止结果(覆盖成功 / 到顶不可达 / 页数上限 / 用户取消)都有明确说明。
@@ -182,19 +232,18 @@
182
232
  - **能力位** `capabilities.turnOutline`:按宿主是否真的挂载投影判定,不按版本号推断;旧插件/旧 App 双方都能降级到既有行为。
183
233
  - 已知限制(如实标注):轮次特别多的长会话,跨页跳转可能失败或流畅度一般。
184
234
 
185
- ##### App 在线本地通知(#46)
235
+ ### App 在线本地通知(#46)
186
236
 
187
237
  - SSE 在线且 App **不在前台**时,`approval/requested` 与 `question/requested` 直接弹系统本地通知(Android 13+ 首次启动请求通知权限);点通知直达该会话的待办卡。
188
238
  - 文案:`需要授权:<工具名>` + 理由;`需要你回答` + 首个问题;同一 `rpcId` 5 分钟内只提醒一次(SSE 重连/回放会重放未结算帧)。
189
239
  - 回到前台自动清掉系统通知(App 内已有卡片与横幅,避免重复打扰);`pushUrls` 退为「App 被杀/离线」时的兜底。
190
240
  - Android 构建开启 core library desugaring(`desugar_jdk_libs 2.1.5`),这是该通知库的硬性要求。
191
241
 
192
- ##### 二维码地址优选(#49)
242
+ ### 二维码地址优选(#49)
193
243
 
194
244
  - 新增纯函数 `orderQrAddresses`:默认仍是「局域网私有地址优先」;**仅当机器上没有任何 `10./192.168.x/172.16-31` 地址时**,才把 Tailscale CGNAT(`100.64/10`)提到最前,避免校园网/热点等场景首选到不可达地址。
195
245
  - 7 项回归覆盖排序与端到端载荷选择;`docs/03-api.md` 同步。
196
246
 
197
-
198
247
  ## v3.2.1-alpha.1(2026-10-24)— Desktop 连接、版本偏斜与 npm 载体预发布
199
248
 
200
249
  > 插件 `3.2.1-alpha.1` / App `3.2.1+24` / 载体 `3.2.1-alpha.1-app`。主插件发布到 npm `alpha` dist-tag;首发后显式将 `latest` 指向 alpha.1,后续预发布不自动移动 `latest`。
@@ -1359,4 +1408,3 @@ lanBridge:
1359
1408
  - `/m` 移动页:登录、发消息、SSE 流式、会话历史
1360
1409
  - 访问口令认证(cookie/header)、Host 校验、二维码
1361
1410
  - 推送桥:Server酱 / ntfy / Bark / generic
1362
-
package/docs/03-api.md CHANGED
@@ -920,11 +920,235 @@ fetch/push 序列化 common domain,pull/sync/abort 同时序列化 common 与
920
920
 
921
921
  **能力位**:`GET /m/api/bootstrap` 与 SSE hello 的 `capabilities.turnOutline` 与本节同一判据(见 §3.1、§3.6):宿主真的注册了投影单元且观察面可用才 `supported: true`,否则 `supported: false`。客户端必须先读能力位再决定是否渲染未加载刻度。
922
922
 
923
-
924
-
925
-
926
923
  ### 6.18 GET /m/api/diagnostics
927
924
 
928
925
  返回当前插件的运行诊断:`plugin` 标识插件名与版本;`runtime` 描述插件进程、WebServer 监听地址、鉴权、LAN 桥及运行指标;`services` 列出探测到的宿主服务;`checks` 提供功能检查结果;`notes` 提供可读提示。
929
926
 
930
927
  `checks.approvalMode` 表示问询/审批策略,`checks.remoteEvents` 表示 `$events` 双端通道是否就绪。旧宿主的 `apiProxy` / `respondBridge` / `frameBridge` 项仅在相应服务被探测到时出现。当前响应不包含宿主版本、`host.capabilities` 或统一受支持范围;不能根据插件版本推断宿主能力。
928
+
929
+ ### 6.19 Git 只读浏览接口(contractVersion 1.1.0,issue #42)
930
+
931
+ > 本节是移动端 Git 只读浏览的**自包含**接口说明(此前的只读 Git 实现没有写入本文档)。全部端点只读、要求鉴权,并且只允许读取「当前会话 `cwd` 所属、且位于已注册 DSH 工作区内的 Git 仓库」。响应不包含 host 发现的仓库绝对路径、remote URL 或作者邮箱;错误只返回稳定 `error` 码,不回传 provider 细节。唯一例外:`/git/file-history` 的 `reason: "path-invalid"` 会原样回显**客户端自己提交的** `path`,供客户端回填/纠错(见 §6.19.8)。对外契约版本 `contractVersion` 自本次起为 **`1.1.0`**:只做加法扩展,旧 App 与外部旧 provider 都不破坏。
932
+
933
+ **公共参数**:`sessionId`(必填);除 `/git/capabilities`、`/git/repository` 外,其余端点还需要 `GET /m/api/git/repository` 返回的 `repositoryId`,它与会话绑定,跨会话/跨仓库复用会得到 `403 repository-not-authorized`。
934
+
935
+ | 方法 | 路径 | 用途 |
936
+ |---|---|---|
937
+ | GET | `/m/api/git/capabilities` | Git 只读能力与契约版本 |
938
+ | GET | `/m/api/git/repository` | 当前会话 cwd 解析出的仓库标识 |
939
+ | GET | `/m/api/git/branches` | 本地/远程分支列表 |
940
+ | GET | `/m/api/git/graph` | 一至五个 tip 之间的提交拓扑(分页) |
941
+ | GET | `/m/api/git/commit` | 提交详情 + 变更文件(分页) |
942
+ | GET | `/m/api/git/worktree` | 工作区快照(已暂存/未暂存/未跟踪) |
943
+ | GET | `/m/api/git/preview` | 单文件文本差异预览(有界) |
944
+ | GET | `/m/api/git/files` | HEAD 已跟踪文件列表(v1.1.0 新增) |
945
+ | GET | `/m/api/git/file-history` | 单文件提交历史(v1.1.0 新增) |
946
+
947
+ #### 6.19.1 GET /m/api/git/capabilities
948
+
949
+ ```json
950
+ {
951
+ "ok": true,
952
+ "git": {
953
+ "contractVersion": "1.1.0",
954
+ "available": true,
955
+ "reason": null,
956
+ "readOnly": true,
957
+ "provider": "bundled",
958
+ "features": {
959
+ "repository": true, "branches": true, "graph": true, "commit": true,
960
+ "worktree": true, "preview": true, "refsDetail": true, "fileHistory": true
961
+ }
962
+ }
963
+ }
964
+ ```
965
+
966
+ | 字段 | 类型 | 说明 |
967
+ |---|---|---|
968
+ | `contractVersion` | string | 宿主 Git 契约版本;本次为 `1.1.0`(`1.0.0` 为旧版) |
969
+ | `available` / `reason` | bool / string\|null | 当前会话是否可用;`reason` 取值 `git-provider-unavailable` / `workspace-registry-unavailable` / `not-git-repository` / `repository-not-authorized` |
970
+ | `readOnly` | bool | 恒为 `true` |
971
+ | `provider` | string | `bundled`(包内只读实现)或 `external`(外部 provider) |
972
+ | `features` | object | 能力位;App 必须按位判断,不得按版本号推断 |
973
+
974
+ v1.1.0 新增两个**相互独立**的能力位:
975
+
976
+ - `refsDetail`:graph/commit 返回结构化 `refsDetail` / `tagsDetail`。宿主总是可以给出名称级结构(旧 provider 的原始 refs 字符串会被解析兜底),因此新宿主恒为 `true`。
977
+ - `fileHistory`:`/git/files` 与 `/git/file-history` 可用。**只看当前生效的 provider**:external provider 必须自己实现 `files()` / `fileHistory()`;bundled provider 还要求其 Git subprocess 与工作区注册表可用。外部 provider 缺少新方法时该位为 `false`,宿主**不会**用 bundled 数据补齐 external 的 graph/commit 视图(两个端点返回 `503 provider-incompatible`)。
978
+
979
+ #### 6.19.2 结构化引用:`refsDetail` / `tagsDetail`(graph 与 commit 共用)
980
+
981
+ graph 的每个提交与 commit 详情都**保留旧的** `refs: string[]`(deprecated,仍为原始 decoration 字符串),并**新增**以下两个数组:
982
+
983
+ ```json
984
+ "refsDetail": [
985
+ { "kind": "head", "name": "HEAD", "shortName": "HEAD", "oid": "<commit>", "current": true },
986
+ { "kind": "localBranch", "name": "refs/heads/main", "shortName": "main", "oid": "<commit>", "current": true },
987
+ { "kind": "remoteBranch", "name": "refs/remotes/origin/main", "shortName": "origin/main", "oid": "<commit>" }
988
+ ],
989
+ "tagsDetail": [
990
+ { "name": "refs/tags/nested", "shortName": "nested", "oid": "<commit>", "annotated": true, "hint": "nested-tag" },
991
+ { "name": "refs/tags/nightly", "shortName": "nightly", "oid": "<commit>", "annotated": false },
992
+ { "name": "refs/tags/v1.0.0", "shortName": "v1.0.0", "oid": "<commit>", "annotated": true,
993
+ "tagger": "Alice", "date": 1700000000, "subject": "Release v1.0.0" }
994
+ ]
995
+ ```
996
+
997
+ | 字段 | 类型 | 说明 |
998
+ |---|---|---|
999
+ | `refsDetail[].kind` | string | `head` / `localBranch` / `remoteBranch` / `other`(tag 不进 refsDetail) |
1000
+ | `refsDetail[].name` / `shortName` | string | 完整 ref 名 / 规范化短名(去掉 `refs/heads/`、`refs/remotes/`;`HEAD` 保持 `HEAD`) |
1001
+ | `refsDetail[].oid` | string | 该引用解析到的提交 OID(与所在提交一致) |
1002
+ | `refsDetail[].current` | bool? | HEAD 与当前本地分支为 `true`;其余省略 |
1003
+ | `tagsDetail[].name` / `shortName` | string | 完整 tag ref 名 / 短名 |
1004
+ | `tagsDetail[].oid` | string | tag 解引用后指向的提交 OID(不是 tag 对象 OID) |
1005
+ | `tagsDetail[].annotated` | bool | 是否为 annotated tag(旧 provider 兜底无法判断时为 `false`) |
1006
+ | `tagsDetail[].tagger` / `date` / `subject` | string? / number? / string? | 仅 commit 详情的 annotated tag 提供;`date` 为 Unix 秒;`subject` 截断到 200 字 |
1007
+ | `tagsDetail[].hint` | string? | 只有 commit 详情可能出现 `nested-tag`(tag→tag→commit;graph 行不返回 `hint`);`non-commit-target` 只属于 provider 契约的 `refs()` 记录(见 §6.19.11),因为目标不是 commit 的 tag 不会出现在任何提交行上 |
1008
+
1009
+ 约定:
1010
+
1011
+ - **顺序**:`refsDetail` 固定 HEAD → 本地分支 → 远程分支 → 其它;`tagsDetail` 按 tag 全名排序。按 `(kind, shortName)`(tag 为 `shortName`)去重。
1012
+ - 排除 `refs/stash` 与 `refs/remotes/*/HEAD` 等符号引用。
1013
+ - **graph 行只保证名称级信息**:`tagsDetail` 条目只含 `name` / `shortName` / `oid` / `annotated`;完整 tag 元数据只在 `commit` 详情返回。
1014
+ - **目标不是 commit 的 tag 不会出现在 graph/commit 的 `tagsDetail`**:它们不指向任何提交行,无法挂在某个提交上;provider 契约仍通过 `refs()` 暴露这些 tag(`hint: "non-commit-target"`)。`nested-tag` 因为最终解引用到提交而出现在对应提交的 `tagsDetail`;graph 行按名称级规则省略该 `hint`。
1015
+ - **失败降级**:annotated tag 元数据读不到时静默降级为「名字 + `annotated`」,不影响列表展示,不产生错误。
1016
+
1017
+ #### 6.19.3 GET /m/api/git/repository、GET /m/api/git/branches
1018
+
1019
+ `repository` 响应:`{ ok, repositoryId, name, headOid?, currentBranch?, detached, empty }`。`headOid` 在空仓库省略;`currentBranch` 为 `refs/heads/*` 全名。
1020
+
1021
+ `branches` 响应:`{ ok, repositoryId, branches: [{ name, displayName, oid, kind, current, tracking, ahead, behind }] }`,`kind` 为 `local` / `remote`,`name` 为完整 ref 名。
1022
+
1023
+ #### 6.19.4 GET /m/api/git/graph
1024
+
1025
+ | 参数 | 说明 |
1026
+ |---|---|
1027
+ | `tips` | JSON 数组字符串:`[{"name":"refs/heads/main"\|"HEAD"\|"refs/remotes/origin/main","tipOid":"<可选>"}]`,1–5 个且不重复 |
1028
+ | `snapshotId` / `cursor` | 翻页时成对提交:首次请求省略,响应返回 `snapshotId` 与 `nextCursor` |
1029
+ | `limit` | 每页条数,1–200,默认 100 |
1030
+
1031
+ 响应:`{ ok, repositoryId, snapshotId, tips, commits, nextCursor }`,`commits[]` 含 `oid` / `parents` / `author` / `timestamp` / `subject` / `refs` / `refsDetail` / `tagsDetail`。引用在翻页期间移动、tip 失效、快照过期或游标非法返回 `409 graph-stale`;非法 tip 返回 `409 graph-tip-invalid`,超过 5 个返回 `400 graph-too-many-tips`。
1032
+
1033
+ #### 6.19.5 GET /m/api/git/commit
1034
+
1035
+ | 参数 | 说明 |
1036
+ |---|---|
1037
+ | `oid` | 提交 OID(必填,40/64 位十六进制) |
1038
+ | `filesCursor` / `filesLimit` | 变更文件翻页游标 / 每页条数(1–200,默认 100) |
1039
+
1040
+ 响应:`{ ok, repositoryId, oid, parents, author, timestamp, message, refs, tags, refsDetail, tagsDetail, stats: { additions, deletions, files }, files: { items, total, nextCursor } }`。`files.items[]` 含 `path` / `oldPath?` / `status`(`added` / `modified` / `deleted` / `renamed` / `copied` / `type-changed`)/ `binary` / `additions` / `deletions`。`tagsDetail` 在此返回完整 annotated tag 元数据(见 §6.19.2)。
1041
+
1042
+ #### 6.19.6 GET /m/api/git/worktree、GET /m/api/git/preview
1043
+
1044
+ - `worktree`:`{ ok, repositoryId, snapshotId, staged, unstaged, untracked, truncated }`。三类条目含 `path` / `oldPath?` / `status` / `conflicted`;快照最多 2000 条,超出 `truncated: true`。
1045
+ - `preview`:`kind=staged|unstaged|untracked` 需同时带 `worktree` 返回的 `snapshotId`;`kind=commit` 需带 `oid`。响应 `{ ok, repositoryId, kind, path, oldPath?, diff, truncated, binary, additions, deletions, notice? }`;单文件差异上限 512 KiB,二进制/符号链接/子模块/纯模式变化以 `notice` 说明。
1046
+
1047
+ #### 6.19.7 GET /m/api/git/files(v1.1.0 新增)
1048
+
1049
+ 读取 **HEAD 已跟踪**文件(不是工作区目录列表,未跟踪文件不会出现)。
1050
+
1051
+ | 参数 | 说明 |
1052
+ |---|---|
1053
+ | `q` | 可选;大小写不敏感的路径子串过滤(服务端过滤) |
1054
+ | `limit` | 1–200,默认 200 |
1055
+
1056
+ ```json
1057
+ { "ok": true, "repositoryId": "repo_…", "files": ["README.md", "docs/guide.md"], "truncated": false }
1058
+ ```
1059
+
1060
+ - `files` 按路径升序;`truncated` 表示还有更多匹配文件未返回(`limit` 截断)。
1061
+ - 空仓库返回 `{ files: [], truncated: false }`(HTTP 200)。
1062
+ - **规模上限**:实现读取一次 `git ls-tree -r --name-only HEAD`,受宿主 subprocess stdout 上限 **8 MiB** 约束;仓库路径总表超过该上限时返回 `413 git-output-too-large`(fail-closed,不返回可能错位/截断的半份列表)。`q` 过滤在宿主拿到完整路径表之后执行,不能降低该上限。
1063
+
1064
+ #### 6.19.8 GET /m/api/git/file-history(v1.1.0 新增)
1065
+
1066
+ 单文件提交历史(仅当前 HEAD 可达范围;不做 `--follow` 重命名跟随)。
1067
+
1068
+ | 参数 | 说明 |
1069
+ |---|---|
1070
+ | `path` | 必填;仓库相对路径或工作区绝对路径(宿主归一化,见下) |
1071
+ | `cursor` | 可选;不透明分页游标(绑定会话/仓库/路径,过期或非法返回 `400 invalid-history-cursor`) |
1072
+ | `limit` | 1–100,默认 20 |
1073
+
1074
+ ```json
1075
+ {
1076
+ "ok": true,
1077
+ "repositoryId": "repo_…",
1078
+ "path": "docs/guide.md",
1079
+ "entries": [
1080
+ { "oid": "…", "shortOid": "…", "author": "Alice", "timestamp": 1700000000, "subject": "update guide", "status": "M" }
1081
+ ],
1082
+ "nextCursor": "…",
1083
+ "truncated": false
1084
+ }
1085
+ ```
1086
+
1087
+ - `entries` 按提交从新到旧;`status` 是该提交对 `path` 的变更类型(Git 单字母)。本机 bundled provider 使用 `git log --name-status -M`(**不带 `--follow`**):重命名在旧路径记录为 `D`、在新路径记录为 `A`,因此 bundled 实际只会产生 `A` / `M` / `D`(以及类型变化的 `T`),不会产生 `R` / `C`;`R` / `C` 只在外部 provider 主动上报时出现。App 按单字母徽标渲染,不得假设 `R` 必然出现。
1088
+ - 空/异常结果**恒为 HTTP 200**,用 `reason` 表达(不让客户端猜 404):
1089
+
1090
+ | `reason` | 场景 |
1091
+ |---|---|
1092
+ | `not-tracked` | HEAD 中不存在该路径(未跟踪/从未提交) |
1093
+ | `no-history` | 仓库尚无提交(空仓库) |
1094
+ | `path-invalid` | 越界(`..`、仓库外绝对路径、无法映射的 Windows 绝对路径)或无法归一化的路径 |
1095
+
1096
+ - **路径归一化由宿主负责**:接受工作区绝对路径或仓库相对路径,归一化到仓库根内;越界/仓库外返回 `path-invalid`。目录路径(含仓库根、`.`)返回 `400 path-invalid`;只接受文件路径。
1097
+ - `reason: "path-invalid"` 的 200 响应中,`path` 为客户端提交的原值(便于回填/纠错);这是客户端输入的回显,不是宿主发现的新路径信息。
1098
+ - **有界**:单次最多扫描 5000 条提交或 20 秒软超时;达到上限时返回已取部分并 `truncated: true`(客户端可提示「仅显示最近 N 条提交」)。
1099
+ - 缺失 `path` → `400 bad-request`。
1100
+
1101
+ #### 6.19.9 Git 只读端点错误码
1102
+
1103
+ | HTTP | error 值 | 场景 |
1104
+ |---|---|---|
1105
+ | 400 | `bad-request` | 缺 `sessionId` / `repositoryId` / `path`,`tips` 不是 JSON |
1106
+ | 400 | `invalid-oid` / `invalid-files-cursor` / `invalid-history-cursor` | 非法 OID / 文件分页游标 / 历史分页游标 |
1107
+ | 400 | `path-invalid` | `/git/file-history` 传入目录路径 |
1108
+ | 400 | `preview-invalid` | 预览参数非法 |
1109
+ | 400 | `graph-tip-invalid` / `graph-too-many-tips` | 非法 tip / 超过 5 个 tip |
1110
+ | 401 | `auth-required` / `bad-token` | 未鉴权(同 §1) |
1111
+ | 403 | `repository-not-authorized` / `workspace-not-allowed` | 会话/仓库不匹配或路径不在已注册工作区 |
1112
+ | 404 | `session-not-found` / `not-git-repository` / `preview-file-not-found` | 会话不存在 / cwd 不是仓库 / 提交中无该文件 |
1113
+ | 409 | `graph-stale` / `graph-tip-invalid` | 快照或 tip 已变化 |
1114
+ | 413 | `git-output-too-large` | Git 输出超过宿主上限(graph/commit/preview 的输出、`/git/files` 的 HEAD 路径总表,均为 8 MiB 级) |
1115
+ | 503 | `git-provider-unavailable` / `provider-incompatible` | provider 不可用 / 旧 provider 不支持所需方法 |
1116
+
1117
+ #### 6.19.10 兼容与降级
1118
+
1119
+ - 契约只做加法:旧 App 忽略新字段即可;旧宿主(无 `refsDetail` / `fileHistory` 能力位)由 App 走字符串解析兜底(`HEAD ->`、`refs/heads/`、`refs/remotes/`、`tag: refs/tags/`)。
1120
+ - 外部 provider 兼容 `1.0.0`:缺 `refs()` 时宿主用原始 refs 字符串合成名称级 `refsDetail` / `tagsDetail`(`annotated` 按 `false`);`/git/files`、`/git/file-history` 只在**当前生效的** provider 实现新方法时生效(bundled provider 还要求 subprocess 可用),否则能力位 `fileHistory=false` 且两个端点 `503 provider-incompatible`,**不会**用 bundled 数据补齐 external provider 的视图。
1121
+ - **成本说明**:graph/commit 读取会额外执行一次 refs 查询(bundled provider 为一次 `for-each-ref`,成本与引用数线性相关;graph 分页每页一次),用于 annotated 元数据与 tag 排序;旧 provider 无 `refs()` 时退回解析 `%D` decoration 字符串。
1122
+ - `contractVersion` 只随破坏性变更提升主/次版本;`1.x` 内的加法不要求客户端按版本号分支。
1123
+
1124
+ #### 6.19.11 外部 provider 契约 v1.1.0(可选加法)
1125
+
1126
+ 宿主通过内核服务 `gitReadProvider` 读取仓库事实;provider 满足 1.0.0 基础方法即可继续被使用,1.1.0 方法全部**可选**。宿主按**当前生效 provider 的方法存在性**决定能力位,不做跨 provider 混用。
1127
+
1128
+ 基础契约(`contractVersion` 为 `1.0.0` 或 `1.1.0`):`capabilities()`、`repositoryRoot(cwd)`、`repositoryInfo(root)`、`branches(root)`、`resolveTip(root, ref)`、`graph(root, { refs, offset, limit })`、`commit(root, oid)`。
1129
+
1130
+ 1.1.0 可选方法(缺失时宿主按能力位降级):
1131
+
1132
+ | 方法 | 签名 | 返回 | 说明 |
1133
+ |---|---|---|---|
1134
+ | `refs` | `refs(root)` | `RefRecord[]` | 全量 ref 记录(见下)。缺失时宿主从 graph/commit 的 `refs` decoration 字符串兜底(annotated 未知,按 `false`) |
1135
+ | `files` | `files(root, { q, limit })` | `{ files: string[], truncated: boolean }` | HEAD 已跟踪路径;已按 `q`(大小写不敏感子串)过滤并按路径排序;最多 `limit` 条,`truncated` 表示还有更多匹配 |
1136
+ | `fileHistory` | `fileHistory(root, path, { maxCommits, timeoutMs })` | `{ entries: HistoryEntry[], truncated: boolean, reason?: string }` | `path` 为仓库相对 POSIX 路径;`entries` 从新到旧;`maxCommits` 默认 5000;`timeoutMs` 为软超时(默认 20000ms),超时返回已取部分且 `truncated: true`;`reason` 仅允许 `not-tracked` / `no-history` / `path-invalid` |
1137
+
1138
+ `RefRecord`(`refs()` 元素):
1139
+
1140
+ | 字段 | 类型 | 说明 |
1141
+ |---|---|---|
1142
+ | `name` | string | `HEAD` / `refs/heads/*` / `refs/remotes/*` / `refs/tags/*` |
1143
+ | `shortName` | string | 规范化短名(不带 `refs/heads/`、`refs/remotes/`、`refs/tags/`) |
1144
+ | `kind` | string | `head` / `localBranch` / `remoteBranch` / `tag` |
1145
+ | `oid` | string | 引用直接指向的对象 OID;annotated tag 为 tag 对象 OID |
1146
+ | `commitOid` | string \| null | 解引用后提交 OID;轻量 tag 等于 `oid`;目标不是 commit 时为 `null` |
1147
+ | `current` | bool | 是否 HEAD / 当前分支 |
1148
+ | `annotated` | bool | tag 是否为 annotated(非 tag 恒 `false`) |
1149
+ | `tagger` / `date` / `subject` | string? / number? / string? | annotated tag 元数据;`date` 为 Unix 秒;`tagger` 只含姓名(不得含邮箱);`subject` 原样(宿主按需截断 200 字) |
1150
+ | `hint` | string? | `nested-tag`(tag→tag→commit)或 `non-commit-target`(最终目标不是 commit) |
1151
+
1152
+ `HistoryEntry`(`fileHistory()` 元素):`{ oid, shortOid, author, timestamp, subject, status }`;`status` 为 Git 单字母(`A`/`M`/`D`/`R`/`C`/`T`,实际出现哪些由 provider 决定);`author` 只含姓名;`timestamp` 为 Unix 秒。provider 只返回有界结果,不应抛出宿主未知的错误码(未知错误会被映射为 `git-provider-unavailable`)。
1153
+
1154
+ 约束:provider 不得返回主机绝对路径、remote URL 或邮箱;路径事实必须限定在 `repositoryRoot` 授权仓库内。