dsh-file-activity 0.5.9 → 0.5.10
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 +17 -211
- package/README.md +24 -79
- package/lib/client.js +28 -1
- package/lib/index.js +8 -0
- package/lib/media-route.js +37 -17
- package/lib/parts/view.part.js +1 -1
- package/lib/state.js +0 -3
- package/lib/store.js +15 -2
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,214 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
本文件记录 dsh-file-activity 的所有版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
-
## [0.5.
|
|
6
|
-
|
|
7
|
-
### 变更
|
|
8
|
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
- chore(
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
- fix(
|
|
16
|
-
- fix
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
- fix(dsh-file-activity): #266 侧边栏「文件活动」面板打开后永远显示「暂无文件活动记录」(P0)——
|
|
24
|
-
宿主渲染 `sidebar.right.pane.tab` 席位时 owner props 是**空对象**
|
|
25
|
-
(`renderSlot(seat, {}, {hookContext})`),可见性只在注入的 `useTabInfo()` 里;
|
|
26
|
-
旧守卫只读 `props.visible`(恒为 `undefined`)→ 守卫恒真 → 面板打开后既不首载也不轮询
|
|
27
|
-
(`POLL_MS` 失效),只有手点「刷新」才有数据。现按宿主真实契约取
|
|
28
|
-
`useTabInfo().tab.visible`(取不到时退回可见:宁可多轮询一个面板,也不能空白),
|
|
29
|
-
切回页签/展开侧边栏立即重新加载并重启轮询,宿主判定不可见时不轮询(省资源)。
|
|
30
|
-
同一 issue 的关联项:
|
|
31
|
-
- B:「新标签页」菜单补 guide 入口(`sidebarRightTabs.register({guide:[…]})`)——此前页签关闭后
|
|
32
|
-
无法从 UI 重新打开(auto-open 每浏览器标签只跑一次);
|
|
33
|
-
- C:HTML 预览按 README 承诺落地为**沙箱 iframe**(此前 `viewerOf()` 无 html 分支,
|
|
34
|
-
`.html` 落到文本渲染器、显示为代码块 `.dfa-fp-code`):iframe
|
|
35
|
-
`sandbox="allow-scripts allow-forms"` + 媒体路由响应头
|
|
36
|
-
`Content-Security-Policy: sandbox allow-scripts allow-forms`(两层都不含
|
|
37
|
-
`allow-same-origin`,直接打开该 URL 同样受限);
|
|
38
|
-
- D:删除客户端挂载探针(此前每次页面加载都往状态文件写一条
|
|
39
|
-
`sessionId="__probe__", path="mounted"` 的幽灵记录,累计 9 行、重启后仍在),
|
|
40
|
-
并在加载时剔除历史残留(#197「状态有界」)。
|
|
41
|
-
- fix(dsh-file-activity): #197 写放大 3,665× 与状态无界(P0)——
|
|
42
|
-
落盘改为 JSON Lines 增量 append(落盘字节 ≈ 事件本体字节)+ 按状态体积自适应的原子 compact;
|
|
43
|
-
新增三维入口上限(会话/每会话路径/全局路径)与 LRU 淘汰(淘汰计数 + warn 可见,非静默丢弃);
|
|
44
|
-
compact 快照显式传 `minIntervalMs`/`maxBytes` 护栏;旧全量 JSON 快照自动兼容并改写。
|
|
45
|
-
实测:单条新事件落盘 789,780 B → 68 B(11,614×↓);稳态每事件 2,104 B → 63 B(33×↓);
|
|
46
|
-
密集流投影 44.18 MB/小时 → 1.94 MB/小时(22.8×↓);内存条目与状态文件均有界。
|
|
47
|
-
本插件并入 `scripts/resource-smoke.mjs` 写放大门禁(此前仅覆盖 observability)。
|
|
48
|
-
|
|
49
|
-
## [0.5.8] - 2026-09-10
|
|
50
|
-
|
|
51
|
-
### 变更
|
|
52
|
-
|
|
53
|
-
- feat(ts): 第四轮 JS→TS 迁移(5 个插件,server + client 全量)
|
|
54
|
-
|
|
55
|
-
## [0.5.7] - 2026-09-07
|
|
56
|
-
|
|
57
|
-
### 变更
|
|
58
|
-
|
|
59
|
-
- fix(deps): update package-lock.json to fix CI/CD failures
|
|
60
|
-
- feat(observability): #155 插件状态查询聚合——统一 status-query 事件 + /plugin-status API (#170)
|
|
61
|
-
- chore(plugins): #165 清理失效的 dsh.client.inject 声明(13 插件) (#167)
|
|
62
|
-
|
|
63
|
-
## [0.5.6] - 2026-09-04
|
|
64
|
-
|
|
65
|
-
### 变更
|
|
66
|
-
|
|
67
|
-
- chore(release): dsh-file-activity v0.5.6(#111 浮窗预览 + 验证清单)
|
|
68
|
-
- fix(file-activity): #111 HTML 预览内容撑满浮窗 (#116)
|
|
69
|
-
- docs: #106 安装命令统一加 --trust-lockfile (#113)
|
|
70
|
-
|
|
71
|
-
## [0.5.5] - 2026-09-02
|
|
72
|
-
|
|
73
|
-
### 变更
|
|
74
|
-
|
|
75
|
-
- fix(scripts): #72 插件依赖未随安装自动安装(dsh-shared 未发布 npm) (#96)
|
|
76
|
-
- fix(file-activity): 浮窗预览失焦自动关闭(issue #76) (#95)
|
|
77
|
-
- fix(file-activity): #72 补充 dsh-better-sidebar 级联安装——dependencies 声明 + 缺失提示
|
|
78
|
-
- style: prettier 全量格式修复(CI 格式门禁)
|
|
79
|
-
|
|
80
|
-
## [Unreleased]
|
|
81
|
-
|
|
82
|
-
### 变更
|
|
83
|
-
|
|
84
|
-
- fix(ui): 浮窗预览失焦自动关闭——点击外部任意处/切换页签即关,预览失败 2.5s 自动关闭或点击任意处关闭,头部「点击外部关闭」提示(issue #76)
|
|
85
|
-
|
|
86
|
-
## [0.5.4] - 2026-09-01
|
|
87
|
-
|
|
88
|
-
### 变更
|
|
89
|
-
|
|
90
|
-
- fix(file-activity): 浮窗预览优雅降级与工作区外文本读取(issue #68)
|
|
91
|
-
|
|
92
|
-
## [0.5.3] - 2026-09-01
|
|
93
|
-
|
|
94
|
-
### 变更
|
|
95
|
-
|
|
96
|
-
- fix(ui): 9 个插件未定义 token danger-primary 改用 error-primary(DSH 主题仅定义 business/error/success/warn)
|
|
97
|
-
|
|
98
|
-
## [0.5.2] - 2026-08-31
|
|
99
|
-
|
|
100
|
-
### 变更
|
|
101
|
-
|
|
102
|
-
- fix(ui): dsh-file-activity 移除页签选中态品牌蓝覆盖,回归宿主默认样式(issue #60)
|
|
103
|
-
- feat(ui): dsh-mermaid-render 卡片翻新——图标/前缀/状态(issue #54)
|
|
104
|
-
- feat(ui): dsh-my-guard 安全护栏面板翻新——图标/前缀/状态/交互(issue #54)
|
|
105
|
-
- feat(ui): 共享图标系统 + UI 规范文档(issue #54 阶段0)
|
|
106
|
-
- refactor(shared): 抽取 dsh-shared 共享工具包,10 个插件迁移消除重复实现(issue #45)
|
|
107
|
-
- chore(deps): 升级 react 19 兼容性——13 个插件 peer 声明 ^18.2.0 || ^19.2.0(issue #49)
|
|
108
|
-
- style(format): 全仓 prettier 格式化(issue #44)
|
|
109
|
-
|
|
110
|
-
## [0.5.1] - 2026-08-27
|
|
111
|
-
|
|
112
|
-
### 变更
|
|
113
|
-
|
|
114
|
-
- feat(file-activity): 侧边栏页签选中态改用品牌蓝,三态一眼可分(issue #25)
|
|
115
|
-
- feat(file-activity): 文件活动列表按文件类型显示专属彩色图标(issue #24)
|
|
116
|
-
|
|
117
|
-
## [0.5.0] - 2026-08-26
|
|
118
|
-
|
|
119
|
-
### 变更
|
|
120
|
-
|
|
121
|
-
- feat(file-activity): bash 命令文件操作识别(rm/touch/mv/cp/tee/重定向)+ 0 值计数徽标过滤
|
|
122
|
-
- docs+test: 全面审查修复——文档同步补全 + mermaid 测试增强
|
|
123
|
-
|
|
124
|
-
## [0.4.7] - 2026-08-25
|
|
125
|
-
|
|
126
|
-
### 变更
|
|
127
|
-
|
|
128
|
-
- **npm 页面元数据优化**:description 改为中英双语(中文在前);README 效果截图引用改为绝对 URL(unpkg),npm 包页面可直接显示图片。
|
|
129
|
-
|
|
130
|
-
## [0.4.6] - 2026-08-25
|
|
131
|
-
|
|
132
|
-
### 变更
|
|
133
|
-
|
|
134
|
-
- **Server 端按 P2 模块拆分**:`lib/index.js`(434 行)按职责拆分为 fence/state/store/observer/api-route/media-route 子模块(入口 50 行),覆盖率/变异统计范围同步扩展至全部 server 文件(Stryker 变异 90.91% ≥ 70%)。
|
|
135
|
-
- **Client 端方案 B 拆分**:`client.src.js` 模板 + 13 个片段(`lib/parts/`)经 `scripts/build.mjs` 拼接生成 `client.js`;eslint 尺寸规则覆盖 src/parts 源码、构建产物排除。
|
|
136
|
-
- **client-render 测试零依赖化**:不再硬编码本机 react 绝对路径,改为自写 createElement stub(CI 跨平台可跑)。
|
|
137
|
-
- 行为不变(纯重构)。
|
|
138
|
-
|
|
139
|
-
## [0.4.5] - 2026-08-24
|
|
140
|
-
|
|
141
|
-
### 修复
|
|
142
|
-
|
|
143
|
-
- **状态文件 `sessions` 为 `null` 时崩溃**:`loadState` 只检查 `typeof sessions !== 'object'`,而 `typeof null === 'object'`——手改/损坏的状态文件若把 `sessions` 写成 `null`,加载后 `Object.values(null)` 抛 TypeError(unhandled rejection)。现在显式把 `null` 一并重置为空对象,降级为全新状态不崩溃(复现测试 `host-mutation.mjs` 已永久保留)。
|
|
144
|
-
|
|
145
|
-
### 变更
|
|
146
|
-
|
|
147
|
-
- **质量门禁测试体系接入**:测试框架迁移至 Vitest(node:test → vitest,API 兼容);覆盖率门禁 `lines≥85 / branches≥75 / functions≥80`(当前实测 99.1/91.8/97.5);新增边界测试 `host-edge.mjs`(12 用例)与变异定向测试 `host-mutation.mjs`(49 用例);Stryker 变异测试达标(71.76% ≥ 70%);Gherkin 验收测试 11 场景/56 步骤(`test/features/`)随 `npm test` 运行。
|
|
148
|
-
|
|
149
|
-
## [0.4.4] - 2026-08-24
|
|
150
|
-
|
|
151
|
-
### 修复
|
|
152
|
-
|
|
153
|
-
- **工作区外文件的图片/PDF 预览不再破图**:浮动预览的媒体字节此前走侧边栏的 `/sidebar/file` 路由,而该路由只允许会话工作区(cwd)内的文件——agent 实际触碰过的 `/tmp` 临时文件等工作区外路径点击预览时一律 403,图片显示为破图、PDF 加载失败。现在媒体字节改由插件自身的 `/file-activity/file` 路由提供(**只放行该会话记录过的路径**:未记录 403、文件不存在 404、超 64MB 拒绝;fence 信任校验与侧边栏一致),图片在浮窗内正常渲染;PDF 因侧边栏内置查看器写死媒体路由,浮窗改用浏览器原生 PDF 帧 + 下载兜底。
|
|
154
|
-
- Markdown / 代码 / HTML 文本预览不受影响(侧边栏 `fs.read` 本身不限制工作区,`client-render.mjs` + 真实浏览器实测回归通过)。
|
|
155
|
-
|
|
156
|
-
## [0.4.3] - 2026-08-23
|
|
157
|
-
|
|
158
|
-
### 变更
|
|
159
|
-
|
|
160
|
-
- **README 补充现场效果图**:顶部新增真实现场效果图(侧边栏「文件活动」页签 最近访问/文件统计树 + 点击文件的浮窗预览),便于其他用户下载前了解功能。
|
|
161
|
-
- **发布包包含效果图**:`package.json` 的 `files` 加入 `assets/`,使 GitHub Release 的 tarball 一并包含效果图资源。
|
|
162
|
-
|
|
163
|
-
## [0.4.2] - 2026-08-23
|
|
164
|
-
|
|
165
|
-
### 修复
|
|
166
|
-
|
|
167
|
-
- **修复页签偶发"纯文字无样式"**:样式注入曾放在 `betterSidebar` 服务判空早退之后,HMR 重建/服务重载瞬间新实例可能跳过注入,导致已渲染的页签失去全部 CSS(纯白文本列表)。样式注入改为 `apply` 无条件最先执行(纯静态 CSS,不依赖任何服务),每个 fiber 持有自己的 `<style>` 元素,重建后至少保留一份。
|
|
168
|
-
|
|
169
|
-
## [0.4.1] - 2026-08-23
|
|
170
|
-
|
|
171
|
-
### 修复
|
|
172
|
-
|
|
173
|
-
- **会话切换不再残留上个会话的文件活动**:前端数据 store 改为按 `sessionId` 分桶(`bySession`),每个会话只渲染自己的数据桶——新建/切换会话瞬间立即显示该会话自己的记录(新会话直接为空),不再在 fetch 完成前显示上一个会话的残留数据;fetch 失败时也不再保留旧数据。切换会话时同时关闭上一个会话遗留的浮窗预览。
|
|
174
|
-
|
|
175
|
-
## [0.4.0] - 2026-08-22
|
|
176
|
-
|
|
177
|
-
### 变更
|
|
178
|
-
|
|
179
|
-
- **预览改为浮窗预览(复用侧边栏内置渲染)**:点击任意文件(最近访问 / 文件统计)弹出**悬浮预览窗**,窗口内容通过 `ctx.betterSidebar.matchFileViewer(path)` 复用侧边栏**内置的 file viewer** 渲染(代码语法高亮 / Markdown 富文本 / 图片 / PDF / HTML),并按 viewer 的 `fetchStrategy` 取内容(`fsRead` / `mediaUrl` / `custom`);不再打开侧边栏编辑器标签,也不自建预览组件。浮窗为轻量交互:**点击浮窗外任意处 / `Esc` 键关闭**,标题栏右上保留一个 `×` 关闭按钮;主体为可滚动区域,长文件或大图在窗内滚动查看。
|
|
180
|
-
- **UI 全面改版**(遵循 better-sidebar 设计语言):全部改用 DSH 语义 token(`--dsw-alias-*` 颜色、`--dsw-font-*` 字形、`--ds-*` 动效),flat 无阴影、hairline 细边、8px 圆角行 + hover 底色;样式表随 activation 注入、fiber teardown 卸载(HMR/禁用无残留)。
|
|
181
|
-
- **间距收紧 + 去冗余标题**:整体走 better-sidebar explorer 的紧凑规范(`2px 6px 8px` 容器、30px 行、box-sizing 缩进),移除内容区顶部的「文件活动」标题(tab 条已命名本页),刷新/清空改为右上角细颗粒图标按钮,内容紧贴 tab 条(更沉浸)。
|
|
182
|
-
- **文件夹 / 文件图标可区分**:目录行用**品牌强调色**的文件夹图标 + 折叠箭头 + **加粗**名字,文件行用**中性淡色**的文件图标 + 常规名字,彩色文件夹 vs 素色文档,一眼可辨。
|
|
183
|
-
- **最近访问只显示文件名**:不再展示完整绝对路径,只显示最后的文件名(悬停仍可见完整路径),减少视觉噪音。
|
|
184
|
-
- 操作类型用状态色徽标(新增 = success / 修改 = warn / 读取 = 品牌强调色),文件统计目录树与文件行改为彩色计数胶囊;文件/文件夹行加入图标与折叠箭头。
|
|
185
|
-
- 时间显示从 `HH:MM:SS` 改为**相对时间**(刚刚 / N 分钟前 / N 小时前 / N 天前 / MM/DD),悬停仍可查看完整时钟时间。
|
|
186
|
-
|
|
187
|
-
## [0.3.0] - 2026-08-22
|
|
188
|
-
|
|
189
|
-
### 变更
|
|
190
|
-
|
|
191
|
-
- 最近访问列表改为 **LRU**:同一文件只保留一条(重复访问移到列表最前、不重复显示),每会话最多保留 **5 条**(原 10 条);历史数据在加载时自动去重。
|
|
192
|
-
- 时间显示改为 **`HH:MM:SS` 时钟格式**(最近访问列表、文件统计行内时间、悬停提示的创建/最近访问时间),不再显示相对时间("刚刚 / x 分钟前")。
|
|
193
|
-
- **最近访问区块可折叠**:点击区块标题展开/收起列表。
|
|
194
|
-
- **文件统计目录树可收纳**:每个文件夹行可点击展开/收起(箭头指示状态),收起后隐藏其子目录与文件。
|
|
195
|
-
|
|
196
|
-
## [0.2.0] - 2026-08-22
|
|
197
|
-
|
|
198
|
-
### 变更
|
|
199
|
-
|
|
200
|
-
- 最近访问列表最多保留 **10 条**(原 300 条),避免列表过长。
|
|
201
|
-
- 文件统计新增时间信息:每个文件行内显示**最近访问时间**(相对时间),悬停可查看**创建时间**(首次接触时间)。
|
|
202
|
-
- 文件统计改为**树形目录**展示:按文件**绝对路径**组织成完整目录树(逐层嵌套,目录节点显示子树汇总计数),连续单子目录的路径链自动**压缩为点号标签**(`a/b/c` → `a.b.c`),不再使用扁平的「根目录」分组与点号拼接标签。
|
|
203
|
-
- 点击文件改为打开**浮窗预览**(不再在侧边栏 tab 栏打开):图片走媒体路由渲染、PDF 内嵌 iframe、文本/代码经 `fs.read` 滚动展示;浮窗内提供「在侧边栏打开」与「关闭」按钮。
|
|
204
|
-
- 浮窗样式改用 DSH 主题变量(`--dsw-alias-*`),深浅主题下均正确显示;文本加载失败时展示具体错误原因(如文件不存在),并支持相对路径按会话 cwd 解析。
|
|
205
|
-
- 浮窗**自动关闭**:鼠标移出浮窗约 0.5 秒后自动消失(移回则取消),无需手动点击关闭。
|
|
206
|
-
|
|
207
|
-
## [0.1.0] - 2026-08-22
|
|
208
|
-
|
|
209
|
-
### 新增
|
|
210
|
-
|
|
211
|
-
- 首个版本:基于 dsh-better-sidebar 的「文件活动」侧边栏页签。
|
|
212
|
-
- **最近访问**:按时间倒序记录文件读取 / 新增 / 修改事件,点击文件用侧边栏原生预览打开。
|
|
213
|
-
- **文件统计**:按文件统计读取 / 新增 / 修改次数,按文件夹平铺展示(多层文件夹以 `.` 拼接)。
|
|
214
|
-
- **默认启用 + 自动打开**:页签默认开启,会话首次打开自动展开(可在设置中关闭)。
|
|
215
|
-
- 状态按会话持久化到 `$DSH_HOME/file-activity.json`(防抖 + 原子写入)。
|
|
5
|
+
## [0.5.10] - 2026-09-16
|
|
6
|
+
|
|
7
|
+
### 变更
|
|
8
|
+
|
|
9
|
+
- test: 存量固定 sleep 收敛(whenReady / drained / 渲染态可观测)
|
|
10
|
+
- docs(清理): 文档瘦身 23765 → 8309 行并固化精简规范
|
|
11
|
+
- chore(artifacts): 同步 8 处共享件产物漂移 + 新增产物一致性门禁(fail-closed)
|
|
12
|
+
- chore(cleanup): 清理 unused-local-variable/useless-expression(含共享件假阳性判定)
|
|
13
|
+
- fix(security): 修复代码扫描 error/warning 告警(5 类规则)
|
|
14
|
+
- fix(test): cucumber-js --import glob 改用双引号,兼容 Windows
|
|
15
|
+
- fix(security): 批量修复 'Insecure temporary file' 安全漏洞
|
|
16
|
+
- fix: 移除未使用的导入,修复告警
|
|
17
|
+
- fix: 修复日志注入,修复告警
|
|
18
|
+
- fix: 移除未使用的stat导入,修复文件系统竞争条件
|
|
19
|
+
- fix: 修复 GitHub Code Scanning 安全漏洞 (43 个)
|
|
20
|
+
- fix(security): fix file system race conditions and missing regex anchor
|
|
21
|
+
- fix(security): use tmp library for secure temporary file creation
|
package/README.md
CHANGED
|
@@ -1,113 +1,58 @@
|
|
|
1
1
|
# dsh-file-activity
|
|
2
2
|
|
|
3
|
+
[](https://github.com/topics/dsh)
|
|
4
|
+
|
|
3
5
|
<div align="center">
|
|
4
6
|
<img alt="文件活动插件截图(最近访问 / 文件统计)" src="https://unpkg.com/dsh-file-activity/assets/screenshot.png" width="340" />
|
|
5
7
|
<br />
|
|
6
8
|
<img alt="浮窗预览:点击文件复用侧边栏内置 Markdown 渲染" src="https://unpkg.com/dsh-file-activity/assets/preview-float.png" width="340" />
|
|
7
9
|
<br />
|
|
8
|
-
<img alt="浮窗 HTML
|
|
10
|
+
<img alt="浮窗 HTML 预览:内容撑满整个浮窗主体" src="https://unpkg.com/dsh-file-activity/assets/preview-float-0.5.6.png" width="340" />
|
|
9
11
|
</div>
|
|
10
12
|
|
|
11
|
-
**DSH
|
|
13
|
+
**dsh-file-activity**:DSH 侧边栏文件活动插件(宿主原生侧边栏扩展点,零第三方侧边栏依赖)——在右侧边栏新增「文件活动」页签,记录 agent 工具与插件自身路由的文件读取/新增/修改事件,提供**最近访问**(LRU)与**文件统计**(树形目录)两块视图,点击文件弹出浮窗预览。
|
|
12
14
|
|
|
13
15
|
## 功能
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
bsfeng.IdeaProjects.my-dsh-plugins ← 连续单子目录压缩为一行
|
|
23
|
-
README.md
|
|
24
|
-
lib/
|
|
25
|
-
index.js
|
|
26
|
-
client.js
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
3. **默认启用**:页签注册后默认开启(无需在设置页勾选),且每个会话首次打开时**自动打开**本页——开关在 **设置 → 插件 → 文件活动**(`settings.plugins.tab`,存储在浏览器 `localStorage`,插件自身持久化)。
|
|
30
|
-
|
|
31
|
-
## 工作原理
|
|
32
|
-
|
|
33
|
-
- **Server 端**(`lib/index.js`):监听 DSH `fs/observed` 事件捕获 agent 的 `read` / `write` / `edit` / `str_replace_editor` / `read_image` 等文件操作;提供 `/file-activity/api` 路由(`stats` / `record` / `clear`);状态按会话持久化到 `$DSH_HOME/file-activity.json`(**JSON Lines 增量 append + 阈值原子 compact**,见下「资源治理」)。
|
|
34
|
-
- `write` 通过每会话的已知文件表自动区分**新增**(首次接触)与**修改**(再次写入)。
|
|
35
|
-
- **Client 端**(`lib/client.js`):通过宿主原生扩展点注册——`ctx.sidebarRightTabs.register({id,kind,title})`(页签类型)+ `ctx.slots.register({name:'sidebar.right.pane.tab',key:id}, Body)`(正文席位);`ctx.documentPreviews.register(...)` 声明本插件认领的文件后缀(元数据 only,字节由宿主 document owner 读取);自动打开走 `ctx.sidebarRight.openTab(kind)`(会话表面未挂载时按 1s 重试,彻底失败会写入 `data-dfa-degraded` 标记并在页签内显示提示,绝不静默失效);浮窗自实现并挂在宿主推荐落点 `shell.overlay` 列表席位(不替换宿主 UI);fetch 拦截只记录插件自身路由 `/file-activity/file` 的预览读取。数据按会话(session)**分桶隔离**(`bySession`),每个会话只渲染自己的记录——新建/切换会话立即显示该会话的数据,**不会残留上一个会话的记录**。预览按后缀分流:图片 / PDF / HTML 走插件媒体路由(`<img>` / 原生 PDF frame / **沙箱 iframe**),其余文本走插件文本路由并由官方 `MarkdownText` / `CodeBlock` 渲染(不可用时退回 `<pre>`)。
|
|
17
|
+
- **最近访问**:按 LRU 记录文件读取/新增/修改事件(agent 工具读写 + 侧边栏打开/编辑保存),每会话最多 5 条、同一文件只出现一次(再次访问移到最前);区块标题可折叠,点击文件弹出浮窗预览。
|
|
18
|
+
- **文件统计**:按文件**绝对路径**组织成树形目录,每个文件显示「读取/新增/修改」次数与相对时间(悬停看完整时钟时间);连续单子目录压缩为点号标签(`a/b/c` → `a.b.c`),文件夹行可展开/收起。
|
|
19
|
+
- **默认启用**:页签注册后默认开启,且每个会话首次打开时自动打开本页(开关见「配置」)。
|
|
20
|
+
- **浮窗预览**:按后缀渲染——代码(宿主 CodeBlock 组件,取不到时退回纯文本 `<pre>`)/ Markdown(宿主官方渲染器)/ 图片 / PDF(浏览器原生查看器)/ 沙箱 HTML;点击窗外、按 `Esc` 或点 `×` 关闭,长内容窗内滚动。
|
|
21
|
+
- **工作区外文件也能预览**:字节(图片 / PDF / HTML)与文本(Markdown / 代码)都先走插件自身媒体路由,按 `(sessionId, path)` 授权(只放行本会话记录过的路径,未记录一律 403);插件拒绝该路径时才回退宿主 `fs.read`。
|
|
22
|
+
- **会话隔离**:按会话分桶存储与展示,新建/切换会话立即显示该会话数据,DSH 重启后历史自动恢复。
|
|
23
|
+
- **bash 命令文件触碰**:解析 bash 工具调用里可静态确定的写 / 删操作(rm / touch / tee / mv / cp / install / sed -i)并计入;解析不确定的(`-t` 重定位、未知命令)不记录,git 等未映射命令不跟踪。
|
|
36
24
|
|
|
37
25
|
## 安装
|
|
38
26
|
|
|
39
|
-
> 💡 **npm 安装(普通用户推荐)**:`dsh plugin --profile web add dsh-file-activity --trust-lockfile
|
|
27
|
+
> 💡 **npm 安装(普通用户推荐)**:`dsh plugin --profile web add dsh-file-activity --trust-lockfile`——无需克隆本仓库;依赖自动级联安装(`dsh-shared` 是唯一 dependencies,侧边栏能力全部来自宿主原生扩展点)。
|
|
40
28
|
|
|
41
29
|
```sh
|
|
42
30
|
# 方式一:dsh plugin(推荐)
|
|
43
|
-
# 1) 克隆本仓库(任意目录)
|
|
44
31
|
git clone https://github.com/baosfeng/my-dsh-plugins.git
|
|
45
|
-
# 2) 以本地 link 方式安装(将 <仓库路径> 替换为上面的克隆目录)
|
|
46
32
|
dsh plugin --profile web add link:<仓库路径>/plugins/dsh-file-activity
|
|
47
33
|
|
|
48
|
-
# 方式二:手动
|
|
49
|
-
#
|
|
50
|
-
#
|
|
51
|
-
# 2) 在 ~/.dsh/profiles/web 下执行 pnpm install
|
|
52
|
-
# 3) 在 ~/.dsh/profiles/web/cordis.patch.yml 增加:
|
|
53
|
-
# - insert:
|
|
54
|
-
# - id: file-activity
|
|
55
|
-
# name: 'dsh-file-activity'
|
|
34
|
+
# 方式二:手动 —— 在 profile package.json dependencies 加
|
|
35
|
+
# "dsh-file-activity": "link:<仓库路径>/plugins/dsh-file-activity"
|
|
36
|
+
# 再 pnpm install,并在 cordis.patch.yml 加 id: file-activity / name: 'dsh-file-activity'
|
|
56
37
|
```
|
|
57
38
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
> 编辑 profile 的 `cordis.patch.yml` 会在运行中通过 Cordis HMR 热挂载 server 端,无需重启 `dsh web`;client 端在页面刷新后生效。
|
|
39
|
+
装完**硬刷新浏览器**(Cmd/Ctrl+Shift+R)。编辑 profile 的 `cordis.patch.yml` 会经 Cordis HMR 热挂载 server 端,无需重启 `dsh web`。
|
|
61
40
|
|
|
62
41
|
## 使用
|
|
63
42
|
|
|
64
|
-
-
|
|
65
|
-
- 点击任意文件行 →
|
|
43
|
+
- 入口:侧边栏「新标签页」(guide)菜单 → 文件活动,或会话开始自动打开。
|
|
44
|
+
- 点击任意文件行 → 浮窗预览,不打开侧边栏编辑器标签。
|
|
66
45
|
- 顶部「刷新」手动重载,「清空」删除当前会话全部记录。
|
|
67
|
-
|
|
68
|
-
### 浮窗预览(复用内置渲染)
|
|
69
|
-
|
|
70
|
-
点击文件(最近访问 / 文件统计中的任意文件行)会打开一个**悬浮预览窗**,窗口由本插件渲染(挂在宿主 `shell.overlay` 浮层席位上,不替换宿主 UI)—— 按后缀分流:图片(svg/png/jpg/gif/webp/avif/bmp/ico)用 `<img>`、PDF 用原生 `<iframe>`、HTML(html/htm/xhtml)用**沙箱 `<iframe>`**,三者都取插件自己的媒体路由 `/file-activity/file`(按 `(sessionId, path)` 授权,因此工作区外的记录路径也能预览);其余文本走插件文本路由 `/file-activity/file?as=text`,由官方 `MarkdownText` / `CodeBlock` 渲染(组件库不可用时退回 `<pre>`)。浮窗为轻量交互:**点击浮窗外任意处、按 `Esc` 或点标题栏右上 `×` 都能关闭**;浮窗主体是可滚动区域,长文件或大图在窗内滚动查看。
|
|
71
|
-
|
|
72
|
-
- **代码**(后台为侧边栏内置 CodeMirror 编辑器):语法高亮 + 行号;支持 预览/编辑 切换与保存。
|
|
73
|
-
- **Markdown**(`.md` / `.markdown`):渲染后的富文本预览,可切换 预览/编辑 模式。
|
|
74
|
-
- **图片**(png / jpg / jpeg / gif / webp / svg / bmp / ico / avif):通过媒体路由渲染。
|
|
75
|
-
- **PDF**:浏览器原生 PDF 查看器(浮窗内嵌,工具栏提供下载)。
|
|
76
|
-
- **HTML**(`.html` / `.htm` / `.xhtml`):**沙箱化 iframe 渲染**,内容**撑满整个浮窗主体**(iframe 随浮窗尺寸自适应、内部滚动,与代码/Markdown/图片预览体验一致,issue #111)。沙箱有两层且都不含 `allow-same-origin`:iframe 的 `sandbox="allow-scripts allow-forms"` 与媒体路由响应头的 `Content-Security-Policy: sandbox allow-scripts allow-forms`(响应头让**直接打开该 URL 也仍是沙箱文档**)。文档因此拿到 opaque origin:脚本/表单可跑(交互式页面可用),但读不到宿主页面的 Cookie、存储与同源 API,也不能导航父窗口。页内相对路径资源按站点根解析(不重写为文件目录,与浏览器直开行为一致)。
|
|
77
|
-
- 其余文件按内置 viewer 的 `matchFileViewer` 回退匹配(如 `binary-download` 下载未知二进制)。
|
|
78
|
-
|
|
79
|
-
> 💡 **工作区外文件也能预览**:文件活动记录的是 agent 实际触碰过的文件,路径可能在会话工作区之外(如 `/tmp` 临时文件)。侧边栏自带的媒体路由只允许工作区内文件,因此图片/PDF 的字节由插件自身的 `/file-activity/file` 路由提供——它按会话记录授权(只放行本会话记录过的路径,未记录的路径一律 403),fence 与侧边栏一致。Markdown/代码文本则仍走侧边栏 `fs.read`(不被工作区限制);HTML 走插件媒体路由的沙箱文档(见上)。
|
|
46
|
+
- 资源上限:每会话路径 ≤ 300 / 全局 ≤ 20000 / 会话数 ≤ 64,超限按 LRU 淘汰最久未活动条目(非静默丢弃,写 warn 日志);审计与写盘按增量追加 + 原子 compact,磁盘用量有界。
|
|
80
47
|
|
|
81
48
|
## 配置
|
|
82
49
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
## 数据说明
|
|
86
|
-
|
|
87
|
-
- 按会话(session)隔离存储(服务端 `$DSH_HOME/file-activity.json` 按 sessionId 分桶 + 前端 `bySession` 分桶展示);**DSH 重启后历史数据自动恢复**(持久化加载);新建/切换会话不残留其他会话的记录。
|
|
88
|
-
|
|
89
|
-
## 资源治理(issue #197)
|
|
90
|
-
|
|
91
|
-
写放大与无界增长防护(9/2 事故同构模式整改):
|
|
92
|
-
|
|
93
|
-
- **增量落盘**:每次记录只追加一行 JSON(`{"s","p","o","t"}`,落盘字节 ≈ 事件本体字节 ×1.0),
|
|
94
|
-
累计行数达到按状态体积自适应的阈值时做一次原子 compact(tmp+rename 重写快照行),
|
|
95
|
-
文件大小与写入量双向有界(实测写放大 1.0×,旧实现 2,073×);
|
|
96
|
-
- **三维入口上限(LRU 淘汰,非静默丢弃)**:每会话路径 ≤ 300、全局路径 ≤ 20000、会话数 ≤ 64;
|
|
97
|
-
超限淘汰最久未活动条目/会话,淘汰计数暴露在 `store.stats()` 且写 warn 日志(首次 + 每 10 次);
|
|
98
|
-
- **显式护栏**:compact 快照受 `minIntervalMs`(体积分级节流)与 `maxBytes`(8MB 硬上限,超限拒绝写 + 告警)约束;
|
|
99
|
-
- **升级兼容**:旧的全量 JSON 快照(单行/多行)仍可读取,首次启动一次性改写为 JSON Lines;
|
|
100
|
-
文件里只有事件行(未 compact)时也能按行序完整重放(不丢数据)。
|
|
101
|
-
- 最近访问列表每会话最多保留 5 条(LRU:同一文件仅一条,重复访问移到最前),文件统计不受条数限制。
|
|
102
|
-
- agent 的 `bash`/`git` 等命令内部的文件触碰不在此跟踪范围(只跟踪 DSH 原生文件工具与侧边栏操作)。
|
|
103
|
-
|
|
104
|
-
## 依赖
|
|
50
|
+
无 profile 配置项(不读 `cordis.patch.yml` 的 `config`);唯一开关是设置页 UI 偏好:
|
|
105
51
|
|
|
106
|
-
|
|
|
107
|
-
|
|
|
108
|
-
|
|
|
109
|
-
| `cordis` | 插件运行时 | 是(宿主提供) |
|
|
52
|
+
| 配置项 | 默认 | 作用 |
|
|
53
|
+
| ------------------------------------------------ | ---- | -------------------------------------------------------------------------- |
|
|
54
|
+
| 会话开始时自动打开(**设置 → 插件 → 文件活动**) | 开 | 每个会话首次打开时自动显示本页;关闭后仍可从侧边栏「新标签页」菜单手动打开 |
|
|
110
55
|
|
|
111
56
|
## 相关文档
|
|
112
57
|
|
|
113
|
-
→ [文件活动追踪模块文档](../../docs/文件活动追踪/概述.md) · [
|
|
58
|
+
→ [文件活动追踪模块文档](../../docs/文件活动追踪/概述.md) · [CHANGELOG](CHANGELOG.md)
|
package/lib/client.js
CHANGED
|
@@ -685,6 +685,33 @@ const icon = {
|
|
|
685
685
|
],
|
|
686
686
|
size,
|
|
687
687
|
),
|
|
688
|
+
// 箭头向上(更新图标):向上的箭头,表示更新操作
|
|
689
|
+
arrowUp: (size = 16) =>
|
|
690
|
+
iconSvg(
|
|
691
|
+
[
|
|
692
|
+
createElement('line', { x1: 12, y1: 19, x2: 12, y2: 5 }),
|
|
693
|
+
createElement('polyline', { points: '5 12 12 5 19 12' }),
|
|
694
|
+
],
|
|
695
|
+
size,
|
|
696
|
+
),
|
|
697
|
+
// 电源关(禁用图标):圆形电源按钮,表示禁用操作
|
|
698
|
+
powerOff: (size = 16) =>
|
|
699
|
+
iconSvg(
|
|
700
|
+
[
|
|
701
|
+
createElement('path', { d: 'M18.36 6.64a9 9 0 1 1-12.73 0' }),
|
|
702
|
+
createElement('line', { x1: 12, y1: 2, x2: 12, y2: 12 }),
|
|
703
|
+
],
|
|
704
|
+
size,
|
|
705
|
+
),
|
|
706
|
+
// 电源开(启用图标):圆形电源按钮,表示启用操作
|
|
707
|
+
powerOn: (size = 16) =>
|
|
708
|
+
iconSvg(
|
|
709
|
+
[
|
|
710
|
+
createElement('path', { d: 'M18.36 6.64a9 9 0 1 1-12.73 0' }),
|
|
711
|
+
createElement('line', { x1: 12, y1: 2, x2: 12, y2: 12 }),
|
|
712
|
+
],
|
|
713
|
+
size,
|
|
714
|
+
),
|
|
688
715
|
}
|
|
689
716
|
|
|
690
717
|
// Common-language / file-type badges (issue #24): brand fill + contrast
|
|
@@ -1317,7 +1344,7 @@ function FileActivityView({
|
|
|
1317
1344
|
dataStore,
|
|
1318
1345
|
}) {
|
|
1319
1346
|
const data = useSyncExternalStore(dataStore.subscribe, dataStore.getSnapshot)
|
|
1320
|
-
const [
|
|
1347
|
+
const [, setCwd] = useState(scope?.cwd || '')
|
|
1321
1348
|
const [error, setError] = useState(false)
|
|
1322
1349
|
const [recentOpen, setRecentOpen] = useState(true)
|
|
1323
1350
|
const [collapsedDirs, setCollapsedDirs] = useState(() => new Set())
|
package/lib/index.js
CHANGED
|
@@ -54,6 +54,13 @@ function bashBaseDirOf(exec, ctx, sessionId) {
|
|
|
54
54
|
const workdir = exec.arguments?.workdir;
|
|
55
55
|
return typeof workdir === 'string' && workdir !== '' ? workdir : sessionCwdOf(ctx, sessionId);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* 装配插件并返回本实例的 store。
|
|
59
|
+
*
|
|
60
|
+
* 返回 store 是为了让宿主/测试拿到**确定性就绪信号**(`await store.whenReady()`,
|
|
61
|
+
* issue #343):此前该插件没有就绪信号,测试只能固定等 50–100ms 赌异步状态加载完成。
|
|
62
|
+
* 返回值是新增能力,调用方忽略它时行为与此前完全一致。
|
|
63
|
+
*/
|
|
57
64
|
export function apply(ctx) {
|
|
58
65
|
const store = createStore(ctx);
|
|
59
66
|
// ── agent-side file operations ──────────────────────────────────────────
|
|
@@ -98,4 +105,5 @@ export function apply(ctx) {
|
|
|
98
105
|
});
|
|
99
106
|
// Tear down on unload: flush pending persistence.
|
|
100
107
|
ctx.effect(() => store.dispose, 'dsh-file-activity: persistence teardown');
|
|
108
|
+
return store;
|
|
101
109
|
}
|
package/lib/media-route.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* fence), the client falls back to this route and receives an fs.read-shaped
|
|
14
14
|
* JSON payload ({ ok, value: { content } }) so the viewer mounts unchanged.
|
|
15
15
|
*/
|
|
16
|
-
import { readFile
|
|
16
|
+
import { readFile } from 'node:fs/promises';
|
|
17
17
|
import { basename, isAbsolute, join } from 'node:path';
|
|
18
18
|
import { writeJson } from 'dsh-shared';
|
|
19
19
|
import { sessionCwdOf } from './cwd.js';
|
|
@@ -113,20 +113,41 @@ function assertMediaParams(sessionId, raw) {
|
|
|
113
113
|
if (sessionId === null || raw === null || raw === '')
|
|
114
114
|
throw mediaError(400, 'sessionId and path are required');
|
|
115
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* 把 fs 读取失败按 errno 分类成 HTTP 状态(#318:commit 76f52ac 引入的 `catch {}` 把
|
|
118
|
+
* 所有错误都吞成 404,丢掉了旧实现的诊断信息——EISDIR 与 EACCES 都被误报为
|
|
119
|
+
* "file not found",而目录明明存在、权限被拒也不是"没找到")。
|
|
120
|
+
*
|
|
121
|
+
* 分类(与同文件既有契约对齐):
|
|
122
|
+
* · EISDIR → 400 'not a file' —— 与加固前 `if (!info.isFile())` 的行为一致
|
|
123
|
+
* · EACCES/EPERM → 403 'permission denied' —— 与信任围栏的 403 语义一致
|
|
124
|
+
* · 其余(含 ENOENT)→ 404 'file not found'
|
|
125
|
+
*
|
|
126
|
+
* TOCTOU 加固保留:不再 stat,只在一次 `readFile` 失败后按 errno 归类——check-then-use
|
|
127
|
+
* 的时间窗不复存在,分类只依赖这次真实失败的原因。
|
|
128
|
+
*/
|
|
129
|
+
function mediaReadError(error, abs) {
|
|
130
|
+
const code = error?.code;
|
|
131
|
+
if (code === 'EISDIR' || code === 'ENOTDIR')
|
|
132
|
+
return mediaError(400, `not a file: ${abs}`);
|
|
133
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
134
|
+
return mediaError(403, `permission denied: ${abs}`);
|
|
135
|
+
return mediaError(404, 'file not found');
|
|
136
|
+
}
|
|
116
137
|
/** stat + read + respond with the file's bytes (bounded by MEDIA_LIMIT). */
|
|
117
138
|
async function serveMedia(response, abs, url) {
|
|
118
|
-
let
|
|
139
|
+
let body;
|
|
119
140
|
try {
|
|
120
|
-
|
|
141
|
+
// 单次 read:不再 stat,避免 check-then-use(#318 前的 TOCTOU 加固)
|
|
142
|
+
body = await readFile(abs);
|
|
121
143
|
}
|
|
122
|
-
catch {
|
|
123
|
-
|
|
144
|
+
catch (error) {
|
|
145
|
+
// 按 errno 分类(目录 → 400;权限 → 403;其余 → 404),不丢诊断信息
|
|
146
|
+
throw mediaReadError(error, abs);
|
|
124
147
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (info.size > MEDIA_LIMIT)
|
|
148
|
+
// Check file size after reading to avoid race condition
|
|
149
|
+
if (body.length > MEDIA_LIMIT)
|
|
128
150
|
throw mediaError(413, 'file too large');
|
|
129
|
-
const body = await readFile(abs);
|
|
130
151
|
const headers = {
|
|
131
152
|
'content-type': mediaTypeForPath(abs),
|
|
132
153
|
'cache-control': 'no-cache',
|
|
@@ -151,17 +172,16 @@ async function serveMedia(response, abs, url) {
|
|
|
151
172
|
* recorded paths only.
|
|
152
173
|
*/
|
|
153
174
|
async function serveText(response, abs) {
|
|
154
|
-
let
|
|
175
|
+
let content;
|
|
155
176
|
try {
|
|
156
|
-
|
|
177
|
+
// Use try-catch instead of stat + readFile to avoid TOCTOU race condition
|
|
178
|
+
content = await readFile(abs, 'utf8');
|
|
157
179
|
}
|
|
158
|
-
catch {
|
|
159
|
-
throw
|
|
180
|
+
catch (error) {
|
|
181
|
+
throw mediaReadError(error, abs);
|
|
160
182
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (info.size > TEXT_LIMIT)
|
|
183
|
+
// Check content length after reading to avoid race condition
|
|
184
|
+
if (content.length > TEXT_LIMIT)
|
|
164
185
|
throw mediaError(413, 'file too large');
|
|
165
|
-
const content = await readFile(abs, 'utf8');
|
|
166
186
|
writeJson(response, 200, { ok: true, value: { content } });
|
|
167
187
|
}
|
package/lib/parts/view.part.js
CHANGED
|
@@ -176,7 +176,7 @@ function FileActivityView({
|
|
|
176
176
|
dataStore,
|
|
177
177
|
}) {
|
|
178
178
|
const data = useSyncExternalStore(dataStore.subscribe, dataStore.getSnapshot)
|
|
179
|
-
const [
|
|
179
|
+
const [, setCwd] = useState(scope?.cwd || '')
|
|
180
180
|
const [error, setError] = useState(false)
|
|
181
181
|
const [recentOpen, setRecentOpen] = useState(true)
|
|
182
182
|
const [collapsedDirs, setCollapsedDirs] = useState(() => new Set())
|
package/lib/state.js
CHANGED
|
@@ -14,9 +14,6 @@ import { readFile } from 'node:fs/promises';
|
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import { createSession, defaultLimits, emptyEvicted } from './quota.js';
|
|
16
16
|
import { trimToQuota } from './quota-trim.js';
|
|
17
|
-
/** 快照元信息行标记 / 快照结束行标记(非会话数据行)。 */
|
|
18
|
-
const META = '~';
|
|
19
|
-
const END = '=';
|
|
20
17
|
/** NUL 字符(路径合法性判定)。 */
|
|
21
18
|
const NUL = String.fromCharCode(0);
|
|
22
19
|
/**
|
package/lib/store.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { createState, foldRecord, loadState, mapOp, stateFile } from './state.js';
|
|
9
9
|
import { createPersist, readAppenderStats } from './persist.js';
|
|
10
|
-
import { emptyEvicted, enforceQuota, normalizeLimits
|
|
10
|
+
import { emptyEvicted, enforceQuota, normalizeLimits } from './quota.js';
|
|
11
11
|
import { statSync } from 'node:fs';
|
|
12
12
|
/** compact 触发下限:快照成本必须远小于触发区间的事件流字节(写放大 ≤ 1.6 的关键)。 */
|
|
13
13
|
const MIN_COMPACT_LINES = 20000;
|
|
@@ -23,7 +23,7 @@ function compactLinesFor(stateBytes) {
|
|
|
23
23
|
const scaled = Math.ceil((stateBytes * 12) / AVG_EVENT_BYTES);
|
|
24
24
|
return Math.min(MAX_COMPACT_LINES, Math.max(MIN_COMPACT_LINES, scaled));
|
|
25
25
|
}
|
|
26
|
-
/** Build the per-apply store: { state, record, schedulePersist, stats, dispose }. */
|
|
26
|
+
/** Build the per-apply store: { state, record, schedulePersist, stats, whenReady, dispose }. */
|
|
27
27
|
export function createStore(ctx, deps = {}) {
|
|
28
28
|
const limits = normalizeLimits(deps.limits);
|
|
29
29
|
const store = {
|
|
@@ -32,6 +32,7 @@ export function createStore(ctx, deps = {}) {
|
|
|
32
32
|
schedulePersist: () => { },
|
|
33
33
|
stats: () => zeroStats(limits),
|
|
34
34
|
dispose: () => { },
|
|
35
|
+
whenReady: () => Promise.resolve(),
|
|
35
36
|
};
|
|
36
37
|
const handle = {
|
|
37
38
|
ctx,
|
|
@@ -39,6 +40,7 @@ export function createStore(ctx, deps = {}) {
|
|
|
39
40
|
store,
|
|
40
41
|
pending: [],
|
|
41
42
|
ready: false,
|
|
43
|
+
readyPromise: Promise.resolve(),
|
|
42
44
|
limits,
|
|
43
45
|
evicted: emptyEvicted(),
|
|
44
46
|
facts: [],
|
|
@@ -49,6 +51,13 @@ export function createStore(ctx, deps = {}) {
|
|
|
49
51
|
compactedEvents: 0,
|
|
50
52
|
compactLinesFixed: deps.compactLines !== undefined,
|
|
51
53
|
};
|
|
54
|
+
// 加载就绪信号(issue #343):onLoaded 完成「磁盘状态合并 + pending 回放」后 resolve。
|
|
55
|
+
// 与 #310/#313/#335 同款能力 —— 该插件此前**没有任何就绪信号**,测试只能固定等 50–100ms 赌加载。
|
|
56
|
+
let markReady = () => { };
|
|
57
|
+
handle.readyPromise = new Promise((resolve) => {
|
|
58
|
+
markReady = resolve;
|
|
59
|
+
});
|
|
60
|
+
handle.markReady = markReady;
|
|
52
61
|
handle.persist = createPersist({
|
|
53
62
|
file: handle.file,
|
|
54
63
|
ctx,
|
|
@@ -61,6 +70,7 @@ export function createStore(ctx, deps = {}) {
|
|
|
61
70
|
store.schedulePersist = () => enqueueFlush(handle, true);
|
|
62
71
|
store.stats = () => snapshotStats(handle);
|
|
63
72
|
store.dispose = () => dispose(handle);
|
|
73
|
+
store.whenReady = () => handle.readyPromise;
|
|
64
74
|
void loadState(handle.file, limits, ctx.logger).then((loaded) => onLoaded(handle, loaded));
|
|
65
75
|
return store;
|
|
66
76
|
}
|
|
@@ -133,6 +143,9 @@ function onLoaded(handle, loaded) {
|
|
|
133
143
|
flushFacts(handle);
|
|
134
144
|
schedulePersist(handle);
|
|
135
145
|
}
|
|
146
|
+
// **顺序关键**:缓冲记录回放之后才兑现就绪信号 ——
|
|
147
|
+
// `await store.whenReady()` 之后查询必含加载期间记录的内容(issue #343)。
|
|
148
|
+
handle.markReady?.();
|
|
136
149
|
}
|
|
137
150
|
/** Apply every record buffered before the state load finished (deferred flush). */
|
|
138
151
|
function drainPending(handle) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-file-activity",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.10",
|
|
4
4
|
"description": "DSH 侧边栏文件活动插件:按 LRU 记录文件读取/新增/修改事件(最近访问),按绝对路径树形统计(文件统计),点击文件浮窗预览(代码高亮/Markdown/图片/PDF)。DSH web plugin: file activity tracker — LRU recent-access list plus per-file read/create/modify counts in a folder tree, with floating preview.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"scripts": {
|
|
15
15
|
"build": "npx tsc -p tsconfig.json && node scripts/build.mjs",
|
|
16
16
|
"typecheck": "npx tsc --noEmit -p tsconfig.json && npx tsc --noEmit -p tsconfig.client.json",
|
|
17
|
-
"test": "npx vitest run --coverage && cucumber-js test/features --import
|
|
17
|
+
"test": "npx vitest run --coverage && cucumber-js test/features --import \"test/features/steps/*.mjs\""
|
|
18
18
|
},
|
|
19
19
|
"exports": {
|
|
20
20
|
".": {
|