dsh-archived-chats 1.4.1 → 1.4.2
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 +14 -0
- package/README.md +8 -2
- package/README.zh-CN.md +7 -2
- package/assets/brand/archive-management-banner.png +0 -0
- package/docs/ARCHITECTURE.en.md +2 -2
- package/docs/ARCHITECTURE.md +2 -2
- package/docs/USER_GUIDE.md +4 -2
- package/docs/USER_GUIDE.zh-CN.md +4 -2
- package/lib/export.js +121 -24
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,20 @@
|
|
|
4
4
|
|
|
5
5
|
Entries describe behavior at each release, not necessarily current behavior. Consult the READMEs and user guides for current usage.
|
|
6
6
|
|
|
7
|
+
## 1.4.2 — 2026-09-22
|
|
8
|
+
|
|
9
|
+
### 中文
|
|
10
|
+
|
|
11
|
+
- 修复在 DSH `0.1.6-alpha.2` 等新版 Host 上启动即崩溃的问题。导出改用 `fflate`,移除 `zip-stream` 及其 `readable-stream@4` 依赖链:该链会请求带斜杠的内置模块名(`process/`),新版 Host 的模块解析层去掉斜杠后对内建模块调用 `createRequire().resolve.paths()`,得到 `null` 并逐项迭代,从而抛出异常并中止整个 Host 启动。此问题与聊天内容无关,插件在被加载时即触发。
|
|
12
|
+
- 导出的 ZIP 改为同步分片压缩:每个条目按 256 KiB 分片推送,分片之间让出事件循环,避免单次压缩长时间占用主线程;分片大小固定,同一导出计划仍产生字节一致的归档。
|
|
13
|
+
- 新增依赖树回归测试,阻止该依赖链被重新引入;新增下载中途取消的测试,确认中止会销毁流并以调用方错误拒绝完成,不再可能把已取消的导出报告为成功。
|
|
14
|
+
|
|
15
|
+
### English
|
|
16
|
+
|
|
17
|
+
- Fix the startup crash on DSH `0.1.6-alpha.2` and later Hosts. Export now uses `fflate`, removing `zip-stream` and its `readable-stream@4` chain: that chain requests a builtin module name with a trailing slash (`process/`), and the newer Host's resolution layer strips the slash, calls `createRequire().resolve.paths()` on a builtin, receives `null`, iterates it, and throws — aborting the whole Host boot. The failure is independent of chat content and fires as soon as the plugin loads.
|
|
18
|
+
- ZIP export now compresses synchronously in 256 KiB slices, yielding to the event loop between slices so a single compression never monopolizes the main thread. The slice size is fixed, so one export plan still produces a byte-identical archive.
|
|
19
|
+
- Add a dependency-tree regression test that blocks that chain from returning, and a mid-download cancellation test proving that abort destroys the stream and rejects completion with the caller's error instead of reporting a cancelled export as successful.
|
|
20
|
+
|
|
7
21
|
## 1.4.1 — 2026-09-21
|
|
8
22
|
|
|
9
23
|
### 中文
|
package/README.md
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
[](https://dsh-insights.com/p/Ultronen/dsh-archived-chats/)
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="assets/brand/archive-management-banner.png" alt="Archive Management: local-first archived-chat center for DeepSeek Harness" width="100%">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
2
7
|
<div align="center">
|
|
3
8
|
|
|
4
9
|
<h1>Archive Management</h1>
|
|
@@ -27,7 +32,7 @@ Archive Management gives DeepSeek Harness a place to find chats hidden from the
|
|
|
27
32
|
|
|
28
33
|
> The English name is **Archive Management**, and the Chinese name is **归档管理**. The installation package remains `dsh-archived-chats`.
|
|
29
34
|
>
|
|
30
|
-
> This document follows the `main` branch. This guide targets 1.4.
|
|
35
|
+
> This document follows the `main` branch. This guide targets 1.4.2, whose export writer no longer carries a dependency that prevents startup on newer Hosts. See the [changelog](CHANGELOG.md) for release history, including 1.4.1's English rename and feedback improvements.
|
|
31
36
|
|
|
32
37
|
## Quick start
|
|
33
38
|
|
|
@@ -91,7 +96,7 @@ Successful exports are validated against the same format and budgets as import b
|
|
|
91
96
|
|
|
92
97
|
## Upgrading from older releases
|
|
93
98
|
|
|
94
|
-
**Names and versions:** Version 1.4.1 uses Archive Management; 1.4.0 labels its English menu Session Archive. Both correspond to **归档管理** in Chinese and are the same plugin. The Chinese entry, package name, install command, and data location are unchanged. Update to 1.4.1 or later and restart DSH to load the renamed English entry.
|
|
99
|
+
**Names and versions:** Version 1.4.2 changes the export writer only; the menu names below are unchanged. Version 1.4.1 uses Archive Management; 1.4.0 labels its English menu Session Archive. Both correspond to **归档管理** in Chinese and are the same plugin. The Chinese entry, package name, install command, and data location are unchanged. Update to 1.4.1 or later and restart DSH to load the renamed English entry.
|
|
95
100
|
|
|
96
101
|
The standalone History and cleanup-preview interfaces have been removed; do not use older screenshots to locate them.
|
|
97
102
|
|
|
@@ -112,6 +117,7 @@ The package declares DSH `>=0.1.0-rc.7`; individual features depend on public Ho
|
|
|
112
117
|
| Session-scoped physical location | Required for session-directory accounting and permanent deletion; shared directories are never used as purge targets. |
|
|
113
118
|
| Public writer | Required for ZIP import and snapshot fallback; restoring an intact original does not rewrite its log. Plain legacy create/append is not enough for safe ZIP restore unless the provider explicitly guarantees exclusive creation. |
|
|
114
119
|
| Modern handles | Fork titles, preview/search, and Recycle Bin moves are supported. ZIP v2 preserves inherited boundaries; import uses create/append/flush/close and a safe session-scoped rollback location. Ordinary v1 ZIPs remain readable, but ambiguous seeded v1 sources are refused rather than flattened. |
|
|
120
|
+
| Host module resolution | DSH `0.1.6-alpha.2` added a resolution layer that fails on any dependency requesting a builtin name with a trailing slash. Before 1.4.2 this package reached `readable-stream@4` through `zip-stream`, so the plugin aborted Host startup as soon as it loaded. 1.4.2 uses `fflate` for export and no longer carries that chain; update before running on those Host versions. |
|
|
115
121
|
|
|
116
122
|
Recycle Bin preview still depends on the original session; a missing original may prevent preview even when a protection snapshot can restore it. See [compatibility and limits](docs/USER_GUIDE.md#compatibility-and-limits).
|
|
117
123
|
|
package/README.zh-CN.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/brand/archive-management-banner.png" alt="归档管理:面向 DeepSeek Harness 的本地优先归档聊天中心" width="100%">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
1
5
|
<div align="center">
|
|
2
6
|
|
|
3
7
|
<h1>归档管理</h1>
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
|
|
27
31
|
> 中文名称为「归档管理」,英文名称为 Archive Management;安装包名始终为 `dsh-archived-chats`。
|
|
28
32
|
>
|
|
29
|
-
> 本文跟随 `main` 分支维护。本文适用于 1.4.
|
|
33
|
+
> 本文跟随 `main` 分支维护。本文适用于 1.4.2,其导出实现不再携带会导致新版 Host 启动失败的依赖;已安装用户请先升级。版本变更见[更新日志](CHANGELOG.md),其中包含 1.4.1 的英文名称统一与交互提示优化。
|
|
30
34
|
|
|
31
35
|
## 快速开始
|
|
32
36
|
|
|
@@ -90,7 +94,7 @@ dsh plugin --profile web update dsh-archived-chats
|
|
|
90
94
|
|
|
91
95
|
## 从旧版升级
|
|
92
96
|
|
|
93
|
-
**名称与版本:** 1.4.1 的英文名称为 Archive Management,1.4.0 英文菜单显示 Session Archive;两者都对应中文「归档管理」,不是不同插件。中文入口、包名、安装命令和数据位置不变。更新至 1.4.1 或更高版本并重启 DSH 后,英文入口名称生效。
|
|
97
|
+
**名称与版本:** 1.4.2 仅更换导出实现,下列菜单名称不变。1.4.1 的英文名称为 Archive Management,1.4.0 英文菜单显示 Session Archive;两者都对应中文「归档管理」,不是不同插件。中文入口、包名、安装命令和数据位置不变。更新至 1.4.1 或更高版本并重启 DSH 后,英文入口名称生效。
|
|
94
98
|
|
|
95
99
|
独立「历史版本」和「预览清理」界面已移除,不应再按旧版截图寻找这些入口。
|
|
96
100
|
|
|
@@ -111,6 +115,7 @@ dsh plugin --profile web update dsh-archived-chats
|
|
|
111
115
|
| 会话独占物理位置 | 会话目录统计和永久删除必需;不把共享目录当作删除目标。 |
|
|
112
116
|
| 公开 writer | ZIP 导入和快照回退恢复必需;恢复完好的原会话不重写日志。仅有旧 create/append 不足以安全恢复 ZIP,除非提供者明确保证独占创建。 |
|
|
113
117
|
| 新版句柄接口 | 分叉支持标题、预览/搜索及移入回收站。ZIP v2 保存继承边界;导入通过 create/append/flush/close 写入,并要求安全的会话独占回滚位置。普通 v1 ZIP 仍可读取,但边界不明的 seeded v1 来源会被拒绝,不会展平。 |
|
|
118
|
+
| Host 模块解析 | DSH `0.1.6-alpha.2` 新增的解析层会在任何依赖请求带斜杠的内置模块名时失败。1.4.2 之前,本插件经 `zip-stream` 引入 `readable-stream@4`,因此插件一被加载就会中止 Host 启动。1.4.2 的导出改用 `fflate`,不再携带该依赖链;请在这些 Host 版本上先升级。 |
|
|
114
119
|
|
|
115
120
|
回收站预览目前仍依赖原会话;原件丢失时,即使保护快照可恢复,也可能无法预览。详见[兼容性与限制](docs/USER_GUIDE.zh-CN.md#兼容性与限制)。
|
|
116
121
|
|
|
Binary file
|
package/docs/ARCHITECTURE.en.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English · [中文](ARCHITECTURE.md) · [User guide](USER_GUIDE.md)
|
|
4
4
|
|
|
5
|
-
This document follows `main`, covering 1.4.
|
|
5
|
+
This document follows `main`, covering 1.4.2 behavior, which switches export ZIP generation to `fflate`; see the [changelog](../CHANGELOG.md) for release history. Runtime code is the authority for interfaces and behavior; user-facing documentation should agree with it.
|
|
6
6
|
|
|
7
7
|
## Product boundary and modules
|
|
8
8
|
|
|
@@ -192,7 +192,7 @@ Named native buttons expand branches and native details/summary disclose timesta
|
|
|
192
192
|
|
|
193
193
|
## ZIP export, import, and restore
|
|
194
194
|
|
|
195
|
-
Export contains a manifest and per-chat session.json/transcript.md with sanitized collision-safe paths. It inspects every selected source exactly once and stages the rendered entries before returning a sequential ZIP stream. The shared complete semantic validator and all importer budgets run first, so a successful export is importable by this plugin and a later invalid source cannot turn an already successful response into a partial backup. Modern reads produce v2 manifest/session records with `source.inheritedEventCount`; legacy reads retain v1. Import accepts ordinary records in both versions, requires matching manifest/record versions, and validates the v2 boundary. An ambiguous seeded v1 source is refused rather than flattened. Both formats exclude attachment bytes and automatic descendant recursion.
|
|
195
|
+
Export contains a manifest and per-chat session.json/transcript.md with sanitized collision-safe paths. It inspects every selected source exactly once and stages the rendered entries before returning a sequential ZIP stream. `fflate` compresses those entries synchronously in 256 KiB slices with an event-loop yield between slices, and the writer no longer depends on `zip-stream`, so it cannot pull in the `readable-stream@4` chain that requests a builtin name with a trailing slash. The shared complete semantic validator and all importer budgets run first, so a successful export is importable by this plugin and a later invalid source cannot turn an already successful response into a partial backup. Modern reads produce v2 manifest/session records with `source.inheritedEventCount`; legacy reads retain v1. Import accepts ordinary records in both versions, requires matching manifest/record versions, and validates the v2 boundary. An ambiguous seeded v1 source is refused rather than flattened. Both formats exclude attachment bytes and automatic descendant recursion.
|
|
196
196
|
|
|
197
197
|
The shared limits are 2,000 sessions, 4,001 entries, 4 MiB manifest, 4 MiB per session JSON, 8 MiB per entry/Markdown, and 256 MiB expanded total. Imported compressed input is limited to 512 MiB. Each JSON document is limited to depth 64, 100,000 nodes, and 4 Mi Unicode code points across strings. Import uses bounded decompression and validates declared/actual sizes, paths, version, generator, JSON budgets, and cross-file identities. It reconciles local entries, data descriptors, and the central directory, including sizes and CRCs; truncation, invalid UTF-8, duplicates, encryption, ZIP64, multi-disk archives, and methods other than Store/Deflate are rejected. Existing IDs are disabled; missing workspaces or attachment references produce warnings. Confirmation lasts ten minutes and is single-use; at most eight previews totaling 128 MiB remain in process. Conflict rechecks and confirmed commits run in the lifecycle queue.
|
|
198
198
|
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](ARCHITECTURE.en.md) · 中文 · [用户指南](USER_GUIDE.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
本文跟随 `main` 分支,功能对应 1.4.
|
|
5
|
+
本文跟随 `main` 分支,功能对应 1.4.2,该版本改用 `fflate` 生成导出 ZIP;版本变更见[更新日志](../CHANGELOG.md)。运行时代码是接口与行为的依据;用户操作说明与本文应保持一致。
|
|
6
6
|
|
|
7
7
|
## 产品边界与模块
|
|
8
8
|
|
|
@@ -192,7 +192,7 @@ apply 只接受凭据,不接受调用方增补会话 ID。每条执行前在
|
|
|
192
192
|
|
|
193
193
|
## ZIP 导出、导入与恢复
|
|
194
194
|
|
|
195
|
-
导出包含 manifest、每条 session.json 和 transcript.md,路径净化并处理重名。它仅检查每个选中来源一次,在返回顺序 ZIP
|
|
195
|
+
导出包含 manifest、每条 session.json 和 transcript.md,路径净化并处理重名。它仅检查每个选中来源一次,在返回顺序 ZIP 流之前暂存渲染后的条目。ZIP 由 `fflate` 同步分片压缩生成,每条目按 256 KiB 分片推送并在分片之间让出事件循环;写入器不再依赖 `zip-stream`,因此不会引入会请求带斜杠内置模块名的 `readable-stream@4`。共用完整语义验证器和全部导入预算先行执行,因此成功导出可被本插件导入,之后的无效来源不会把已成功响应变成不完整备份。现代读取产生 v2 manifest/session 记录,保存 `source.inheritedEventCount`;旧读取保留 v1。导入兼容两版的普通记录,要求包与记录版本一致,校验 v2 继承边界。边界不明的 seeded v1 来源会被拒绝,不会展平。两版均不打包附件二进制或自动递归加入后代会话。
|
|
196
196
|
|
|
197
197
|
共用限制为 2,000 条会话、4,001 个条目、4 MiB manifest、单会话 JSON 4 MiB、单条目/Markdown 8 MiB,以及解压总量 256 MiB。导入的压缩输入上限为 512 MiB。每个 JSON 文档限制深度 64、节点 100,000 个,所有字符串合计 4 Mi Unicode 码点。导入有界解压,验证声明/实际大小、路径、版本、生成器、JSON 预算及跨文件身份。它对齐本地条目、数据描述符和中央目录,包括大小与 CRC;拒绝截断、无效 UTF-8、重复项、加密、ZIP64、多磁盘及 Store/Deflate 之外的压缩方法。已有 ID 禁用;缺失工作区或附件引用给警告。确认凭据有效 10 分钟、单次使用,进程最多保留 8 份、总计 128 MiB。确认后的冲突检查和提交进入生命周期队列。
|
|
198
198
|
|
package/docs/USER_GUIDE.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
English · [简体中文](USER_GUIDE.zh-CN.md) · [Back to README](../README.md)
|
|
4
4
|
|
|
5
|
-
Archive Management provides a place to browse and manage DSH's archived chats, plus workspace bulk archiving. This document follows the `main` branch. This guide targets 1.4.
|
|
5
|
+
Archive Management provides a place to browse and manage DSH's archived chats, plus workspace bulk archiving. This document follows the `main` branch. This guide targets 1.4.2, whose export writer no longer carries a dependency that prevents startup on newer Hosts. See the [changelog](../CHANGELOG.md) for release history, including 1.4.1's English rename and feedback improvements. For interfaces and data formats, see the [architecture](ARCHITECTURE.en.md).
|
|
6
6
|
|
|
7
7
|
## Understand the three locations
|
|
8
8
|
|
|
@@ -133,6 +133,8 @@ Import requires a complete, internally consistent ZIP and rejects truncation, CR
|
|
|
133
133
|
|
|
134
134
|
The browser buffers and validates the complete ZIP before initiating its download. The response-byte cap is 320 MiB and the complete fetch/body-read timeout is five minutes; this is not a peak-heap guarantee, and the current non-stream fallback has no stronger universal WebView memory promise. Download started does not mean the browser has saved the file to disk. A timeout or cap failure downloads no incomplete file and can be retried.
|
|
135
135
|
|
|
136
|
+
Export compresses the staged entries in 256 KiB slices and yields to the event loop between them, so a large backup does not freeze the rest of the Host while it is written. When a download is cancelled, the archive stops and the response fails; a cancelled export is never reported as a completed one.
|
|
137
|
+
|
|
136
138
|
ZIP import and Recycle Bin recovery are separate workflows. A ZIP is not a Recycle Bin protection snapshot.
|
|
137
139
|
|
|
138
140
|
## Storage and automatic cleanup
|
|
@@ -173,7 +175,7 @@ When a newer version is found, Get update beside the title opens the plugin mark
|
|
|
173
175
|
|
|
174
176
|
## Upgrades, old data, and downgrades
|
|
175
177
|
|
|
176
|
-
**Names and versions:** Version 1.4.1 uses Archive Management; 1.4.0 labels its English menu Session Archive. Both correspond to **归档管理** in Chinese and are the same plugin. The Chinese entry, package name, install command, and data location are unchanged. Update to 1.4.1 or later and restart DSH to load the renamed English entry.
|
|
178
|
+
**Names and versions:** Version 1.4.2 changes the export writer only; the menu names below are unchanged. Version 1.4.1 uses Archive Management; 1.4.0 labels its English menu Session Archive. Both correspond to **归档管理** in Chinese and are the same plugin. The Chinese entry, package name, install command, and data location are unchanged. Update to 1.4.1 or later and restart DSH to load the renamed English entry.
|
|
177
179
|
|
|
178
180
|
The standalone History and cleanup-preview interfaces are retired. Archiving no longer creates versions. Recycle Bin protection snapshots support recovery; they are not a browsable version-history library.
|
|
179
181
|
|
package/docs/USER_GUIDE.zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[English](USER_GUIDE.md) · 简体中文 · [返回 README](../README.zh-CN.md)
|
|
4
4
|
|
|
5
|
-
归档管理为 DSH 已归档聊天提供查看和管理页面,并扩展按工作区批量归档。本文跟随 `main` 分支维护。本文适用于 1.4.
|
|
5
|
+
归档管理为 DSH 已归档聊天提供查看和管理页面,并扩展按工作区批量归档。本文跟随 `main` 分支维护。本文适用于 1.4.2,其导出实现不再携带会导致新版 Host 启动失败的依赖;已安装用户请先升级。版本变更见[更新日志](../CHANGELOG.md),其中包含 1.4.1 的英文名称统一与交互提示优化。接口和数据格式见[架构说明](ARCHITECTURE.md)。
|
|
6
6
|
|
|
7
7
|
## 先理解三个位置
|
|
8
8
|
|
|
@@ -133,6 +133,8 @@ JSON 是机器可读记录,Markdown 是可读副本。新版 Host 导出 ZIP v
|
|
|
133
133
|
|
|
134
134
|
浏览器会在发起下载前缓冲并验证完整 ZIP。响应字节上限为 320 MiB,完整获取/读取响应体的超时为五分钟;这不是峰值堆内存保证,当前非流式兼容路径也没有更强的通用 WebView 内存承诺。“已开始下载”不表示浏览器已将文件保存到磁盘。超时或超限时不会下载不完整文件,可重试。
|
|
135
135
|
|
|
136
|
+
导出按 256 KiB 分片压缩已暂存的条目,并在分片之间让出事件循环,因此生成大型备份时不会长时间占用 Host 主线程。下载被取消时归档立即停止且响应失败,已取消的导出不会被报告为完成。
|
|
137
|
+
|
|
136
138
|
ZIP 导入和回收站恢复是两套不同流程,不要把 ZIP 当作回收站保护快照使用。
|
|
137
139
|
|
|
138
140
|
## 空间与自动清理
|
|
@@ -173,7 +175,7 @@ DSH 运行时约每分钟检查,关闭期间暂停,启动恢复完成后补
|
|
|
173
175
|
|
|
174
176
|
## 升级、旧数据与降级
|
|
175
177
|
|
|
176
|
-
**名称与版本:** 1.4.1 的英文名称为 Archive Management,1.4.0 英文菜单显示 Session Archive;两者都对应中文「归档管理」,不是不同插件。中文入口、包名、安装命令和数据位置不变。更新至 1.4.1 或更高版本并重启 DSH 后,英文入口名称生效。
|
|
178
|
+
**名称与版本:** 1.4.2 仅更换导出实现,下列菜单名称不变。1.4.1 的英文名称为 Archive Management,1.4.0 英文菜单显示 Session Archive;两者都对应中文「归档管理」,不是不同插件。中文入口、包名、安装命令和数据位置不变。更新至 1.4.1 或更高版本并重启 DSH 后,英文入口名称生效。
|
|
177
179
|
|
|
178
180
|
独立「历史版本」和「预览清理」界面已退役。归档不再创建历史版本;回收站保护快照只用于恢复,不是用户可以浏览的历史版本库。
|
|
179
181
|
|
package/lib/export.js
CHANGED
|
@@ -2,7 +2,8 @@ import {
|
|
|
2
2
|
deriveEventMessage,
|
|
3
3
|
isAppendSurfaceEvent,
|
|
4
4
|
} from '@deepseek-ai/dsh-session';
|
|
5
|
-
import
|
|
5
|
+
import { Readable } from 'node:stream';
|
|
6
|
+
import { Zip, ZipDeflate, strToU8 } from 'fflate';
|
|
6
7
|
import { IMPORT_LIMITS, validateBackupSemantics, validateJsonValue } from './import.js';
|
|
7
8
|
|
|
8
9
|
const EXPORT_FORMAT = 'dsh-archived-chats/export';
|
|
@@ -302,13 +303,122 @@ function formattedJson(value) {
|
|
|
302
303
|
return `${JSON.stringify(value, null, 2)}\n`;
|
|
303
304
|
}
|
|
304
305
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
306
|
+
/**
|
|
307
|
+
* Bytes handed to one synchronous deflate call. fflate compresses on the calling
|
|
308
|
+
* thread, so a whole entry at once would stall the event loop for the length of
|
|
309
|
+
* its compression. Slicing bounds each stall to a few milliseconds while leaving
|
|
310
|
+
* the archive byte-for-byte deterministic for a given plan.
|
|
311
|
+
*/
|
|
312
|
+
const ZIP_ENTRY_SLICE_BYTES = 256 * 1024;
|
|
313
|
+
const ZIP_COMPRESSION_LEVEL = 9;
|
|
314
|
+
const ZIP_STREAM_HIGH_WATER_MARK = 64 * 1024;
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Bridge fflate's callback-driven archive into a Node readable. fflate pushes
|
|
318
|
+
* synchronously from `push()`, so the chunks are queued and drained through the
|
|
319
|
+
* readable's own backpressure instead of being forwarded directly.
|
|
320
|
+
*/
|
|
321
|
+
function createArchiveStream(staged, entryDate) {
|
|
322
|
+
const state = {
|
|
323
|
+
queue: [],
|
|
324
|
+
parked: null,
|
|
325
|
+
halted: false,
|
|
326
|
+
started: false,
|
|
327
|
+
failure: null,
|
|
328
|
+
finalized: false,
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
const stream = new Readable({
|
|
332
|
+
highWaterMark: ZIP_STREAM_HIGH_WATER_MARK,
|
|
333
|
+
read() {
|
|
334
|
+
if (state.parked !== null) {
|
|
335
|
+
const resume = state.parked;
|
|
336
|
+
state.parked = null;
|
|
337
|
+
resume();
|
|
338
|
+
}
|
|
339
|
+
if (!state.started) {
|
|
340
|
+
state.started = true;
|
|
341
|
+
void pump();
|
|
342
|
+
}
|
|
343
|
+
},
|
|
344
|
+
destroy(error, done) {
|
|
345
|
+
// Release a producer parked on backpressure, otherwise the pump would wait
|
|
346
|
+
// for a read that a destroyed stream never issues.
|
|
347
|
+
state.halted = true;
|
|
348
|
+
if (state.parked !== null) {
|
|
349
|
+
const resume = state.parked;
|
|
350
|
+
state.parked = null;
|
|
351
|
+
resume();
|
|
352
|
+
}
|
|
353
|
+
done(error);
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const archive = new Zip((error, chunk) => {
|
|
358
|
+
if (error) {
|
|
359
|
+
state.failure ??= error;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (chunk !== null && chunk.byteLength > 0) state.queue.push(chunk);
|
|
311
363
|
});
|
|
364
|
+
|
|
365
|
+
const stopped = () => state.halted || stream.destroyed;
|
|
366
|
+
|
|
367
|
+
/** Push one chunk, waiting for the consumer when the buffer is full. */
|
|
368
|
+
const emit = async (chunk) => {
|
|
369
|
+
if (stopped()) return false;
|
|
370
|
+
if (stream.push(chunk)) return true;
|
|
371
|
+
await new Promise((resolve) => { state.parked = resolve; });
|
|
372
|
+
return !stopped();
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
/** Forward everything fflate produced for the current push. */
|
|
376
|
+
const drain = async () => {
|
|
377
|
+
while (state.queue.length > 0) {
|
|
378
|
+
const chunk = state.queue.shift();
|
|
379
|
+
if (!(await emit(chunk))) return false;
|
|
380
|
+
}
|
|
381
|
+
return !stopped();
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const pump = async () => {
|
|
385
|
+
try {
|
|
386
|
+
for (const entry of staged) {
|
|
387
|
+
const file = new ZipDeflate(entry.name, { level: ZIP_COMPRESSION_LEVEL });
|
|
388
|
+
file.mtime = entryDate;
|
|
389
|
+
archive.add(file);
|
|
390
|
+
const bytes = strToU8(entry.source);
|
|
391
|
+
if (bytes.byteLength === 0) {
|
|
392
|
+
file.push(bytes, true);
|
|
393
|
+
if (!(await drain())) return;
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
for (let offset = 0; offset < bytes.byteLength; offset += ZIP_ENTRY_SLICE_BYTES) {
|
|
397
|
+
const end = Math.min(offset + ZIP_ENTRY_SLICE_BYTES, bytes.byteLength);
|
|
398
|
+
const last = end >= bytes.byteLength;
|
|
399
|
+
file.push(bytes.subarray(offset, end), last);
|
|
400
|
+
if (!(await drain())) return;
|
|
401
|
+
// Yield between slices so other requests keep their turn on the loop.
|
|
402
|
+
if (!last && !stopped()) await new Promise((resolve) => { setImmediate(resolve); });
|
|
403
|
+
}
|
|
404
|
+
if (stopped()) return;
|
|
405
|
+
}
|
|
406
|
+
archive.end();
|
|
407
|
+
if (!(await drain())) return;
|
|
408
|
+
if (state.failure !== null) throw state.failure;
|
|
409
|
+
state.finalized = true;
|
|
410
|
+
stream.push(null);
|
|
411
|
+
} catch (error) {
|
|
412
|
+
stream.destroy(error);
|
|
413
|
+
}
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const completion = new Promise((resolve, reject) => {
|
|
417
|
+
stream.once('end', resolve);
|
|
418
|
+
stream.once('error', reject);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
return { stream, completion, archive };
|
|
312
422
|
}
|
|
313
423
|
|
|
314
424
|
/**
|
|
@@ -365,28 +475,15 @@ export async function createExportZip({ plan, inspect, generatorVersion, limits:
|
|
|
365
475
|
throw Object.assign(new Error('export source is invalid'), { code: 'export-source-invalid', errors: semanticErrors });
|
|
366
476
|
}
|
|
367
477
|
|
|
368
|
-
const archive = new ZipStream({ level: 9 });
|
|
369
478
|
const entryDate = new Date(plan.exportedAt);
|
|
370
|
-
|
|
371
|
-
const completion = new Promise((resolve, reject) => {
|
|
372
|
-
archive.once('end', resolve);
|
|
373
|
-
archive.once('error', reject);
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const write = async () => {
|
|
377
|
-
for (const entry of staged) await addZipEntry(archive, entry.source, entry.name, entryDate);
|
|
378
|
-
archive.finalize();
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
void write().catch((error) => {
|
|
382
|
-
archive.destroy(error);
|
|
383
|
-
});
|
|
479
|
+
const { stream, completion, archive } = createArchiveStream(staged, entryDate);
|
|
384
480
|
|
|
385
481
|
return {
|
|
386
|
-
stream
|
|
482
|
+
stream,
|
|
387
483
|
completion,
|
|
388
484
|
abort(error = new Error('export aborted')) {
|
|
389
|
-
archive.
|
|
485
|
+
archive.terminate();
|
|
486
|
+
stream.destroy(error);
|
|
390
487
|
},
|
|
391
488
|
};
|
|
392
489
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-archived-chats",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.2",
|
|
4
4
|
"description": "查看、搜索和恢复已归档会话,支持按工作区批量归档、备份导入导出与回收站管理。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ultronen",
|
|
@@ -105,8 +105,7 @@
|
|
|
105
105
|
}
|
|
106
106
|
},
|
|
107
107
|
"dependencies": {
|
|
108
|
-
"fflate": "^0.8.3"
|
|
109
|
-
"zip-stream": "^7.0.5"
|
|
108
|
+
"fflate": "^0.8.3"
|
|
110
109
|
},
|
|
111
110
|
"devDependencies": {
|
|
112
111
|
"@deepseek-ai/dsh-session": "0.1.5-rc.2",
|