@deepseek-ai/dsh-session-log-export 0.1.1-rc.2 → 0.1.2-alpha.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/README.i18n.yaml CHANGED
@@ -2,5 +2,5 @@
2
2
  # side as of the last confirmed-consistent state. Both languages carry equal authority;
3
3
  # after editing either side, bring the other along and re-record with:
4
4
  # pnpm run verify-translation-pairing --write packages/session-query/session-log-export/README.md
5
- README.md: f3625338e85a38ab279c5fa4700a7a9e1124cfb3
6
- README.zh.md: 101f0bb41c7296916b72a82186dae74b1b576e7f
5
+ README.md: a46bcdcd6fa6dda8997b3ed07f0ae789d8e3471d
6
+ README.zh.md: 348f6c09c8c5de4e8fe7f779b6d0fd4f662e8e34
package/README.md CHANGED
@@ -1,31 +1,103 @@
1
+ ---
2
+ description: "Web Session-log ZIP export: Host streaming, the authenticated download route, the Session Header action, and the /export command."
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-session-log-export
2
7
 
3
8
  English | [中文](README.zh.md)
4
9
 
5
- Web Session-log download control over the host-streamed ZIP endpoint owned by `dsh-host-apiproxy`. The Host half registers `/export`; the browser half owns a 111×32 `Session log` action in the Session Header, one download controller, and one modal shared by that button and the slash command. ZIP generation, raw JSONL/zstd reads, descendants, attachments, backpressure, and HTTP error semantics remain owned by the [ApiProxy download implementation](../../host/apiproxy/README.md).
10
+ ## Summary
6
11
 
7
- ## Command contract
12
+ `dsh-session-log-export` lets the Web interface download a session's full history: a `Session log` button in the Session Header and an `/export` slash command both hand the session tree — the session, its sub-sessions, and attachments — to the browser as a ZIP download. The package owns the Host archive stream, its authenticated Fetch route, and the browser controls and feedback. The browser chooses the download destination. Setup and usage come first; implementation details follow.
8
13
 
9
- | Input | Result |
10
- |---|---|
11
- | `/export` | Record a human-command lifecycle; the submitting browser receives the local execution acknowledgment and downloads `GET /api/session.export?sessionId=<id>&includeDescendants=true`. |
12
- | `/export <path>` | Return an error. Browser downloads choose their destination through the browser's ordinary download behavior. |
14
+ ## Table of Contents
15
+
16
+ - [Use this package](#use-this-package)
17
+ - [Understand the implementation](#understand-the-implementation)
18
+ - [Further Exploration](#further-exploration)
19
+ - [Model Experience](#model-experience)
20
+ - [Known Limitations and Deferred Work](#known-limitations-and-deferred-work)
21
+ - [Dev Note](#dev-note)
22
+
23
+ -----
13
24
 
14
- The command is mounted only by the Web bundle. The local `command/executed` acknowledgment triggers the slash download only after a successful `/export` result in the browser that submitted it; other tabs still render the durable command row without repeating the browser side effect. The Header button calls the same controller directly. Both entry paths issue a `HEAD` preflight, then hand the GET URL to the browser download manager without buffering the ZIP in JavaScript; they share in-flight collapsing, cancellation of the preflight on plugin disposal, preparation-error handling, browser save behavior, and the same Modal.
25
+ <a id="use-this-package"></a>
26
+ ## Use this package
15
27
 
16
- The Host download endpoint flushes a live root Session before `readRaw`, so a slash-triggered ZIP includes the `command/run` and `command/done` pair whose acknowledgment started the download. Cold persisted Sessions require no flush.
28
+ Use this package when the Web bundle should let users export a session log. It requires Connection, the command registry, Session query and persistence, and attachments. Mount the plugin, then click `Session log` in the Session Header or type `/export`; the browser downloads `dsh-session-<id>.zip`.
17
29
 
18
- The modal reports preparation, download start, or failure. Closing it does not cancel an in-flight download and does not reopen it when that operation later settles. One Session admits one active download at a time; repeated gestures share that operation.
30
+ ### When to choose it
19
31
 
20
- ## Composition
32
+ Choose it for a Web deployment that needs user-facing session export with a visible download dialog. Avoid it when a programmatic or Host-side export is needed: this package produces a browser download, not a Host path write, and it requires a persistence backend that stores a per-session raw artifact (the shipped JSONL backend supports plaintext and zstd; SQLite export is not supported).
33
+
34
+ ### Composition
21
35
 
22
36
  ```yaml
23
37
  - id: session-log-download
24
38
  name: '@deepseek-ai/dsh-session-log-export'
25
39
  ```
26
40
 
27
- The Web bundle mounts the package beside `dsh-host-apiproxy`, `dsh-commands`, `dsh-client-ui-commands`, and `dsh-client-ui-conversation`. The package contributes its button and modal to the right-aligned `conversation.session.header.utilities` list, independently of the title-adjacent mode, Subagent, and Task entries in `conversation.session.header.actions`; Trajectory carries no export control.
41
+ The Web bundle mounts the package with Connection, `dsh-commands`, `dsh-client-ui-commands`, and `dsh-client-ui-conversation`.
42
+
43
+ ### Configuration
44
+
45
+ | Field | Default | Meaning |
46
+ |---|---|---|
47
+ | `compressionLevel` | `6` | DEFLATE level from 0 through 9 for each ZIP entry. |
48
+
49
+ ### Command contract
50
+
51
+ | Input | Result |
52
+ |---|---|
53
+ | `/export` | Records a human-command lifecycle; the submitting browser downloads `GET /api/session.export?sessionId=<id>&includeDescendants=true` |
54
+ | `/export <path>` | An error; browser downloads choose their destination through the browser's ordinary download behavior |
55
+
56
+ ### What to expect
57
+
58
+ The dialog reports three phases: preparing, download started, or failed. Closing the dialog does not cancel an in-flight download, and the dialog does not reopen when that operation later settles. One session admits one active download at a time; repeated gestures share that operation. The export includes the live session's newest events: the host endpoint flushes a live root session before reading, so a slash-triggered ZIP includes the `command/run` and `command/done` pair that started the download; cold persisted sessions need no flush.
59
+
60
+ ### Failures
61
+
62
+ The dialog shows a preparation error when the preflight fails before ZIP streaming starts — for example an unreachable or misconfigured host endpoint. A descendant or attachment read failure after the browser accepts the GET is reported by the browser download manager, not by the dialog.
63
+
64
+ -----
65
+
66
+ <a id="understand-the-implementation"></a>
67
+ ## Understand the implementation
68
+
69
+ <details>
70
+ <summary>Implementation internals — click to expand</summary>
71
+
72
+ This section explains how the package wires the export control and points at the code that realizes it; the observable behavior is fully covered in [Use this package](#use-this-package).
28
73
 
74
+ ### Design split
75
+
76
+ The package has two halves. The Host half ([`src/index.ts`](src/index.ts)) registers the `/export` command and contributes the exact `GET`/`HEAD /api/session.export` Fetch route to Connection; [`src/archive.ts`](src/archive.ts) builds the bounded ZIP stream. The browser half ([`src/client/index.ts`](src/client/index.ts)) provides the shared download controller and UI, and observes `command/executed` so only the submitting browser starts a download.
77
+
78
+ ### Download flow
79
+
80
+ Both entry paths issue a `HEAD` preflight to `GET /api/session.export?...`, then hand the GET URL to the browser download manager without buffering the ZIP in JavaScript. One controller owns one in-flight download per session, collapses concurrent gestures into that operation, and cancels the preflight on plugin disposal. Modal state lives in a snapshot store keyed by session, so the button and the command share one dialog per session.
81
+
82
+ The Host route is a feature-owned exact Fetch contribution. Connection applies its Host/Origin and browser-session checks and bridges the streaming `Response`; this package owns query validation, live-session flushes, raw artifact and attachment reads, ZIP generation, and HTTP status semantics.
83
+
84
+ </details>
85
+
86
+ -----
87
+
88
+ <a id="further-exploration"></a>
89
+ ## Further Exploration
90
+
91
+ Read these pages when the package-level contract is not enough. They move from the Web control to the host endpoint and the surrounding command and session surfaces.
92
+
93
+ - [dsh-client-connection](../../client/connection/README.md) — the authenticated Fetch-route carrier used by the Host endpoint.
94
+ - [Commands subsystem reference](../../../docs/subsystems/commands.md) — the human-command registry the `/export` command registers on.
95
+ - [dsh-client-ui-commands](../../client/ui-commands/README.md) — the browser command surface that renders and acknowledges `/export`.
96
+ - [Session Query package map](../README.md) — the retrieval family this package belongs to.
97
+
98
+ -----
99
+
100
+ <a id="model-experience"></a>
29
101
  ## Model Experience
30
102
 
31
103
  ### Human `/export` control
@@ -44,6 +116,25 @@ None. The log-only command lifecycle and browser download do not change the deri
44
116
 
45
117
  ## Known Limitations and Deferred Work
46
118
 
47
- - The download endpoint requires a persistence backend with a per-Session raw artifact. The shipped JSONL backend supports plaintext and zstd artifacts; SQLite export is not included in this change.
48
- - This is a browser download, not a Host-path writer. The browser chooses the local destination; no Host path or native folder action is returned.
49
- - The preflight reports failures found before ZIP streaming starts. A descendant or attachment failure after the browser accepts the GET is reported by the browser download manager, not by the modal.
119
+ <a id="known-limitations-and-deferred-work"></a>
120
+
121
+
122
+ These limits define when this package is a poor fit or needs special operational care. They are current package constraints, not a task backlog.
123
+
124
+ - **Requires a per-session raw artifact backend** — the download endpoint needs a persistence backend with a per-session raw artifact; the shipped JSONL backend supports plaintext and zstd, and SQLite export is not supported.
125
+ - **Browser download, not a Host-path writer** — the browser chooses the local destination; no Host path or native folder action is returned.
126
+ - **Preflight reports only pre-stream failures** — a descendant or attachment failure after the browser accepts the GET is reported by the browser download manager, not by the dialog.
127
+
128
+ <a id="dev-note"></a>
129
+ ### Dev Note
130
+
131
+ <details>
132
+ <summary>Working context for maintainers — click to expand</summary>
133
+
134
+ This Dev Note is working context for maintainers: open design questions and directions that are not decided. It is explicitly non-authoritative — shipped behavior, limits, and accepted rationale live in the sections above, the package code, and the linked pages.
135
+
136
+ #### Future: export destinations beyond the browser
137
+
138
+ The download is deliberately browser-scoped; a Host-path or native folder export would need a new endpoint contract and a decision on where the ZIP lands.
139
+
140
+ </details>
package/README.zh.md CHANGED
@@ -1,31 +1,103 @@
1
+ ---
2
+ description: "Web 会话日志 ZIP 导出:Host 流式传输、认证下载路由、Session Header 操作与 /export 命令。"
3
+ kind: "package-reference"
4
+ ---
5
+
1
6
  # @deepseek-ai/dsh-session-log-export
2
7
 
3
8
  [English](README.md) | 中文
4
9
 
5
- Web Session 日志下载控制,使用 `dsh-host-apiproxy` 拥有的 Host 流式 ZIP 端点。Host 半包注册 `/export`;浏览器半包在 Session Header 中提供 111×32 的 `Session log` 操作,以及一个供该按钮与斜杠命令共用的下载控制器和弹窗。ZIP 生成、原始 JSONL/zstd 读取、子 Session、附件、背压和 HTTP 错误语义仍由 [ApiProxy 下载实现](../../host/apiproxy/README.zh.md)负责。
10
+ ## 概述
6
11
 
7
- ## 命令约定
12
+ `dsh-session-log-export` 让 Web 界面可以下载会话的完整历史:Session Header 中的 `Session log` 按钮与 `/export` 斜杠命令都会把会话树——会话本身、其子会话与附件——作为 ZIP 交给浏览器下载。本包拥有 Host 归档流、经过认证的 Fetch 路由以及浏览器控制和反馈。下载目标位置由浏览器选择。设置与用法在前,随后说明实现细节。
8
13
 
9
- | 输入 | 结果 |
10
- |---|---|
11
- | `/export` | 记录一组用户命令生命周期;提交命令的浏览器收到本地执行确认后,下载 `GET /api/session.export?sessionId=<id>&includeDescendants=true`。 |
12
- | `/export <path>` | 返回错误。浏览器下载通过浏览器的普通下载行为选择目标位置。 |
14
+ ## 目录
15
+
16
+ - [使用本包](#use-this-package)
17
+ - [理解实现](#understand-the-implementation)
18
+ - [进一步探索](#further-exploration)
19
+ - [模型体验](#model-experience)
20
+ - [已知限制与延期工作](#known-limitations-and-deferred-work)
21
+ - [开发备注](#dev-note)
22
+
23
+ -----
13
24
 
14
- 该命令只由 Web bundle 挂载。只有 `/export` 返回成功时,本地 `command/executed` 确认才会在提交命令的浏览器中触发斜杠下载;其他标签页仍会渲染持久命令行,但不会重复执行浏览器副作用。Header 按钮直接调用同一个控制器。两种入口都会先发出 `HEAD` 预检,再把 GET URL 交给浏览器下载管理器,JavaScript 不会缓冲 ZIP;它们共用并发折叠、插件释放时取消预检、准备阶段错误处理、浏览器保存行为和同一个 Modal。
25
+ <a id="use-this-package"></a>
26
+ ## 使用本包
15
27
 
16
- Host 下载端点会在 `readRaw` flush 活动的根 Session,因此斜杠命令触发的 ZIP 会包含启动下载的 `command/run` `command/done` 事件对。冷持久化 Session 不需要 flush。
28
+ Web bundle 需要让用户导出会话日志时使用本包。它需要 Connection、命令注册表、Session 查询与持久化以及附件服务。挂载插件,然后点击 Session Header 中的 `Session log` 或输入 `/export`;浏览器会下载 `dsh-session-<id>.zip`。
17
29
 
18
- 弹窗报告准备中、开始下载或失败。关闭弹窗不会取消正在进行的下载;该操作随后完成时也不会重新打开弹窗。每个 Session 同时只允许一项下载,重复操作会共用该任务。
30
+ ### 何时选择
19
31
 
20
- ## 组合
32
+ 为需要带可见下载弹窗的用户级会话导出的 Web 部署选择它。需要程序化或 Host 侧导出时避免使用:本包产生的是浏览器下载,而非 Host 路径写入,并且它要求持久化后端保存逐会话原始产物(随附 JSONL 后端支持明文与 zstd;不支持 SQLite 导出)。
33
+
34
+ ### 组合
21
35
 
22
36
  ```yaml
23
37
  - id: session-log-download
24
38
  name: '@deepseek-ai/dsh-session-log-export'
25
39
  ```
26
40
 
27
- Web bundle 将本包与 `dsh-host-apiproxy`、`dsh-commands`、`dsh-client-ui-commands` 和 `dsh-client-ui-conversation` 一起挂载。本包把按钮和弹窗贡献到最右侧的 `conversation.session.header.utilities` 列表,与标题旁 `conversation.session.header.actions` 中的模式、Subagent 和 Task 配置项相互独立;Trajectory 不包含导出入口。
41
+ Web bundle 将本包与 Connection、`dsh-commands`、`dsh-client-ui-commands` 和 `dsh-client-ui-conversation` 一起挂载。
42
+
43
+ ### 配置
44
+
45
+ | 字段 | 默认值 | 含义 |
46
+ |---|---|---|
47
+ | `compressionLevel` | `6` | 每个 ZIP 条目的 DEFLATE 级别,范围为 0 到 9。 |
48
+
49
+ ### 命令约定
50
+
51
+ | 输入 | 结果 |
52
+ |---|---|
53
+ | `/export` | 记录一组用户命令生命周期;提交命令的浏览器下载 `GET /api/session.export?sessionId=<id>&includeDescendants=true` |
54
+ | `/export <path>` | 错误;浏览器下载通过浏览器的普通下载行为选择目标位置 |
55
+
56
+ ### 预期行为
57
+
58
+ 弹窗报告三个阶段:准备中、开始下载或失败。关闭弹窗不会取消正在进行的下载,该操作随后完成时弹窗也不会重新打开。每个会话同时只允许一项下载,重复操作共用该任务。导出包含实时会话的最新事件:Host 端点在读取前会 flush 活动的根会话,因此斜杠命令触发的 ZIP 会包含启动下载的 `command/run` 与 `command/done` 事件对;冷持久化会话不需要 flush。
59
+
60
+ ### 失败
61
+
62
+ 当 ZIP 流式传输开始前的预检失败时——例如 Host 端点不可达或配置错误——弹窗显示准备阶段错误。浏览器接受 GET 后发生的子会话或附件读取失败由浏览器下载管理器报告,不通过弹窗报告。
63
+
64
+ -----
65
+
66
+ <a id="understand-the-implementation"></a>
67
+ ## 理解实现
68
+
69
+ <details>
70
+ <summary>实现细节——点击展开</summary>
71
+
72
+ 本节解释本包如何接线导出控制,并指出实现它的代码位置;可观察行为已在[使用本包](#use-this-package)中完整说明。
28
73
 
74
+ ### 设计拆分
75
+
76
+ 本包有两个半包。Host 半包([`src/index.ts`](src/index.ts))注册 `/export` 命令,并向 Connection 贡献精确的 `GET`/`HEAD /api/session.export` Fetch 路由;[`src/archive.ts`](src/archive.ts) 构建有界 ZIP 流。浏览器半包([`src/client/index.ts`](src/client/index.ts))提供共享下载控制器和 UI,并观察 `command/executed`,因此只有提交命令的浏览器会启动下载。
77
+
78
+ ### 下载流程
79
+
80
+ 两条入口都会对 `GET /api/session.export?...` 发出 `HEAD` 预检,然后把 GET URL 交给浏览器下载管理器,JavaScript 不缓冲 ZIP。一个控制器按会话持有一项进行中的下载,把并发操作折叠进该任务,并在插件释放时取消预检。弹窗状态存放在按会话键控的快照存储中,因此按钮与命令按会话共享一个弹窗。
81
+
82
+ Host 路由是业务拥有的精确 Fetch contribution。Connection 应用 Host/Origin 与浏览器会话检查并桥接流式 `Response`;本包拥有查询校验、活动会话 flush、原始产物与附件读取、ZIP 生成和 HTTP 状态语义。
83
+
84
+ </details>
85
+
86
+ -----
87
+
88
+ <a id="further-exploration"></a>
89
+ ## 进一步探索
90
+
91
+ 当包级约定不够用时阅读以下页面。它们从 Web 控制逐步进入 Host 端点与周围的命令和会话表面。
92
+
93
+ - [dsh-client-connection](../../client/connection/README.zh.md)——Host 端点使用的认证 Fetch 路由载体。
94
+ - [命令子系统参考](../../../docs/subsystems/commands.zh.md)——`/export` 命令注册的用户命令注册表。
95
+ - [dsh-client-ui-commands](../../client/ui-commands/README.zh.md)——渲染并确认 `/export` 的浏览器命令表面。
96
+ - [会话查询包映射](../README.zh.md)——本包所属的检索能力家族。
97
+
98
+ -----
99
+
100
+ <a id="model-experience"></a>
29
101
  ## 模型体验
30
102
 
31
103
  ### 用户 `/export` 控制
@@ -40,10 +112,29 @@ Web bundle 将本包与 `dsh-host-apiproxy`、`dsh-commands`、`dsh-client-ui-co
40
112
 
41
113
  #### KV Cache 影响
42
114
 
43
- 无。仅日志命令生命周期和浏览器下载不会改变派生请求前缀。
115
+ 无。仅日志命令生命周期与浏览器下载不会改变派生请求前缀。
116
+
117
+ ## 已知限制与延期工作
118
+
119
+ <a id="known-limitations-and-deferred-work"></a>
120
+
121
+
122
+ 这些限制说明本包何时不合适,或何时需要特别的运维注意。它们是当前包约束,不是任务积压。
123
+
124
+ - **要求逐会话原始产物后端**——下载端点需要带逐会话原始产物的持久化后端;随附 JSONL 后端支持明文与 zstd,不支持 SQLite 导出。
125
+ - **浏览器下载,而非 Host 路径写入**——目标位置由浏览器选择;不会返回 Host 路径或原生文件夹操作。
126
+ - **预检只报告流式传输前的失败**——浏览器接受 GET 后发生的子会话或附件读取失败由浏览器下载管理器报告,不通过弹窗报告。
127
+
128
+ <a id="dev-note"></a>
129
+ ### 开发备注
130
+
131
+ <details>
132
+ <summary>维护者的工作上下文——点击展开</summary>
133
+
134
+ 本开发备注是维护者的工作上下文:开放设计问题与尚未决定的探索方向。它明确不具权威性——已交付的行为、限制与既定理由以上文、包代码和相关页面为准。
135
+
136
+ #### 未来:浏览器之外的导出目标
44
137
 
45
- ## 已知限制与暂缓事项
138
+ 下载刻意限定在浏览器范围;Host 路径或原生文件夹导出需要新的端点约定,并决定 ZIP 的落盘位置。
46
139
 
47
- - 下载端点要求持久化后端具有逐 Session 原始工件。随附 JSONL 后端支持明文和 zstd 工件;本次改动不包含 SQLite 导出。
48
- - 这是浏览器下载,不是 Host 路径写入。目标位置由浏览器选择,不会返回 Host 路径或原生文件夹操作。
49
- - 预检只报告 ZIP 开始流式传输前发现的失败。浏览器接受 GET 后发生的子 Session 或附件读取失败由浏览器下载管理器报告,不通过弹窗报告。
140
+ </details>
package/lib/client.js CHANGED
@@ -4,7 +4,7 @@ window.__ModuleLoader__.load({
4
4
  var module = { exports: {} };
5
5
  var exports = module.exports;
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
- let _deepseek_ai_dsh_client_runtime_client = require("@deepseek-ai/dsh-client-runtime/client");
7
+ let _deepseek_ai_dsh_client_store = require("@deepseek-ai/dsh-client-store");
8
8
  let react_jsx_runtime = require("react/jsx-runtime");
9
9
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
10
10
  //#region lib/types/client/controller.js
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
42
42
  fetcher;
43
43
  save;
44
44
  /** uSES-safe state source shared by every Session-scoped modal contribution. */
45
- store = (0, _deepseek_ai_dsh_client_runtime_client.createSnapshotStore)(INITIAL);
45
+ store = (0, _deepseek_ai_dsh_client_store.createSnapshotStore)(INITIAL);
46
46
  active = /* @__PURE__ */ new Map();
47
47
  disposed = false;
48
48
  /**
@@ -187,7 +187,7 @@ window.__ModuleLoader__.load({
187
187
  * @returns the persistent Header action and Session-scoped dialog.
188
188
  */
189
189
  function SessionLogDownloadHeaderAction(props) {
190
- const { sessionId, useSessionLogDownload, request } = props;
190
+ const { sessionId, useSessionLogDownload, request, t } = props;
191
191
  const busy = useSessionLogDownload((state) => state.bySession[String(sessionId)])?.status === "downloading";
192
192
  return (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [(0, react_jsx_runtime.jsxs)("button", {
193
193
  type: "button",
@@ -197,7 +197,7 @@ window.__ModuleLoader__.load({
197
197
  onClick: () => {
198
198
  request(sessionId);
199
199
  },
200
- children: [(0, react_jsx_runtime.jsx)("span", { children: "Session log" }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { size: 12 })]
200
+ children: [(0, react_jsx_runtime.jsx)("span", { children: t("header.action") }), (0, react_jsx_runtime.jsx)(_deepseek_ai_dsh_client_ui_primitives.IconDownloadOutline16, { size: 12 })]
201
201
  }), (0, react_jsx_runtime.jsx)(SessionLogDownloadDialog, { ...props })] });
202
202
  }
203
203
  //#endregion
@@ -206,6 +206,7 @@ window.__ModuleLoader__.load({
206
206
  const NS = "session-log-download";
207
207
  /** Simplified-Chinese Session export strings. */
208
208
  const zh = {
209
+ "header.action": "Session 日志",
209
210
  "dialog.preparingTitle": "正在导出 Session",
210
211
  "dialog.preparingDescription": "正在准备包含当前 Session、子 Session 和附件的 ZIP 文件。",
211
212
  "dialog.successTitle": "Session 导出已开始下载",
@@ -216,6 +217,7 @@ window.__ModuleLoader__.load({
216
217
  };
217
218
  /** English Session export strings. */
218
219
  const en = {
220
+ "header.action": "Session log",
219
221
  "dialog.preparingTitle": "Exporting Session",
220
222
  "dialog.preparingDescription": "Preparing a ZIP containing this Session, its sub-Sessions, and attachments.",
221
223
  "dialog.successTitle": "Session download started",
package/lib/index.js CHANGED
@@ -1,16 +1,380 @@
1
+ import Schema from "@deepseek-ai/schemastery";
2
+ import { brandString } from "@deepseek-ai/dsh-brand";
3
+ import { Zip, ZipDeflate } from "fflate";
4
+ //#region lib/types/archive.js
5
+ /**
6
+ * Host-side session-log download: streams one ZIP archive whose files are the
7
+ * sessions' stored artifact text verbatim plus every referenced media object.
8
+ * The root artifact sits under its original base name (`session.jsonl`); each
9
+ * subagent descendant under `subagents/<id>/<filename>`; each image referenced
10
+ * by any included log under `media/<attachmentId>.<ext>` (content-addressed,
11
+ * so one archive never duplicates a shared image). No manifest is written —
12
+ * every file is byte-identical to the backend's durable artifact or attachment
13
+ * store and self-describing through its own header line or media type. Before
14
+ * each live session's artifact read, the SessionStore flush barrier makes the
15
+ * current in-memory log durable; cold sessions need no barrier. Request abort
16
+ * and response-consumer cancellation share one producer signal and terminate
17
+ * the active compressor.
18
+ * Compression runs on the host with fflate's streaming Zip API, so the archive
19
+ * bytes are produced incrementally and the host never holds the whole archive
20
+ * in one buffer; production waits for consumer pull whenever the response queue
21
+ * reaches its byte high-water mark, so a slow consumer bounds accumulation to
22
+ * the fixed 64 KiB response queue plus one synchronous fflate push.
23
+ * @module
24
+ */
25
+ /** Balanced default used when Session export configuration omits a compression level. */
26
+ const DEFAULT_SESSION_LOG_COMPRESSION_LEVEL = 6;
27
+ /**
28
+ * Resolve the persistence, session-query, and attachment services a log export needs.
29
+ * @param ctx - the composed host context.
30
+ * @returns the export services (absent when the deployment does not mount them).
31
+ */
32
+ function sessionLogExportDeps(ctx) {
33
+ return {
34
+ sessionQuery: ctx.get("sessionQuery"),
35
+ sessionPersistence: ctx.get("sessionPersistence"),
36
+ attachments: ctx.get("attachments"),
37
+ sessions: ctx.get("sessions")
38
+ };
39
+ }
40
+ /**
41
+ * Flush one currently live session through the store's authoritative durability
42
+ * barrier immediately before its raw artifact is read. A cold or absent id has
43
+ * no in-memory work to flush.
44
+ * @param deps - export services, including the optional live-session store.
45
+ * @param id - the session whose artifact is about to be read.
46
+ * @param signal - optional cancellation observed around the flush barrier.
47
+ */
48
+ async function flushLiveSessionLog(deps, id, signal) {
49
+ signal?.throwIfAborted();
50
+ const sessions = deps.sessions;
51
+ if (sessions === void 0) return;
52
+ const session = sessions.get(id);
53
+ if (session === void 0) return;
54
+ await sessions.flush(session);
55
+ signal?.throwIfAborted();
56
+ }
57
+ /** Zip extension for each accepted raster media type. */
58
+ const MEDIA_TYPE_EXTENSIONS = {
59
+ "image/png": "png",
60
+ "image/jpeg": "jpg",
61
+ "image/webp": "webp",
62
+ "image/gif": "gif"
63
+ };
64
+ /**
65
+ * The zip path for one media object: content-addressed by the opaque
66
+ * attachment id so shared images land once and the id in the log maps back to
67
+ * the archive entry without a manifest.
68
+ * @param ref - the durable reference from a session log.
69
+ * @returns the archive path.
70
+ */
71
+ function mediaEntryPath(ref) {
72
+ return `media/${String(ref.attachmentId)}.${MEDIA_TYPE_EXTENSIONS[ref.mediaType]}`;
73
+ }
74
+ /**
75
+ * Collect every image reference inside one content array, descending into
76
+ * nested tool results the way the live attachment route does.
77
+ * @param content - an event content array (or nested tool-result content).
78
+ * @param refs - the dedupe map being filled (keyed by attachment id).
79
+ */
80
+ function collectImageRefs(content, refs) {
81
+ if (!Array.isArray(content)) return;
82
+ const pending = [];
83
+ for (const item of content) pending.push(item);
84
+ while (pending.length > 0) {
85
+ const value = pending.pop();
86
+ if (typeof value !== "object" || value === null || Array.isArray(value)) continue;
87
+ const block = value;
88
+ if (block.type === "image" && typeof block.attachment === "object" && block.attachment !== null) {
89
+ const ref = block.attachment;
90
+ refs.set(String(ref.attachmentId), ref);
91
+ }
92
+ if (Array.isArray(block.content)) for (const item of block.content) pending.push(item);
93
+ }
94
+ }
95
+ /**
96
+ * Collect every image reference one session event carries, across the same
97
+ * carriers the live attachment route scans (direct content, message content,
98
+ * inserted messages, and completed assistant chunk blocks).
99
+ * @param event - one parsed JSONL event object.
100
+ * @param refs - the dedupe map being filled (keyed by attachment id).
101
+ */
102
+ function collectEventImageRefs(event, refs) {
103
+ const data = event.data;
104
+ if (typeof data !== "object" || data === null) return;
105
+ const carrier = data;
106
+ collectImageRefs(carrier.content, refs);
107
+ if (carrier.message !== void 0) collectImageRefs(carrier.message.content, refs);
108
+ if (carrier.inserted !== void 0) for (const message of carrier.inserted) collectImageRefs(message.content, refs);
109
+ if (carrier.chunk?.type === "block-end") collectImageRefs([carrier.chunk.block], refs);
110
+ }
111
+ /**
112
+ * Collect the distinct media references one stored artifact text names.
113
+ * Lines that fail to parse cannot reference media and are skipped (the
114
+ * artifact text itself is exported verbatim regardless).
115
+ * @param content - the stored artifact text.
116
+ * @returns the dedupe map keyed by attachment id.
117
+ */
118
+ function imageRefsInArtifact(content) {
119
+ const refs = /* @__PURE__ */ new Map();
120
+ for (const line of content.split("\n")) {
121
+ if (line === "") continue;
122
+ let event;
123
+ try {
124
+ event = JSON.parse(line);
125
+ } catch {
126
+ continue;
127
+ }
128
+ collectEventImageRefs(event, refs);
129
+ }
130
+ return refs;
131
+ }
132
+ /**
133
+ * One safe zip path segment from an untrusted session id. Session ids are
134
+ * host-controlled, but the brand allows any non-empty string, so `../`, dot
135
+ * segments, and separator characters are neutralized before they can shape
136
+ * archive entries. Distinct ids may collapse onto one segment (id collision
137
+ * is impossible for the host-minted UUIDs, so no uniqueness suffix is kept).
138
+ * @param id - the raw session id.
139
+ * @returns a filesystem-safe single path segment.
140
+ */
141
+ function safeSessionIdSegment(id) {
142
+ return id.replace(/[^A-Za-z0-9_-]/g, "_");
143
+ }
144
+ /**
145
+ * The export archive filename for one root session.
146
+ * @param sessionId - the root session id (sanitized to one safe path segment).
147
+ * @returns the attachment filename for the session's export archive.
148
+ */
149
+ function sessionLogZipFilename(sessionId) {
150
+ return `dsh-session-${safeSessionIdSegment(sessionId)}.zip`;
151
+ }
152
+ /**
153
+ * Yield the export entries in zip order: the preloaded root artifact first,
154
+ * then every subagent descendant in lineage order (each flushed when live,
155
+ * read from the persistence backend right before it is yielded, and dropped
156
+ * after the consumer moves on), then every distinct media object referenced by any of
157
+ * the included logs (read and verified from the attachment store, one archive
158
+ * entry per attachment id). The host holds at most one descendant's artifact
159
+ * text and one media object at a time beyond the root.
160
+ * @param deps - the mounted export services (the caller answered 500 before this runs).
161
+ * @param root - the already-read root artifact (read by the caller so the
162
+ * missing-session path can answer cleanly before streaming starts).
163
+ * @param sessionId - the root session id.
164
+ * @param includeDescendants - whether to include every subagent descendant.
165
+ * @param signal - optional cancellation forwarded to lineage, persistence, and attachment reads.
166
+ * @returns the export entries in zip order.
167
+ */
168
+ async function* sessionLogZipEntries(deps, root, sessionId, includeDescendants, signal) {
169
+ const media = /* @__PURE__ */ new Map();
170
+ const rememberMedia = (content) => {
171
+ for (const [id, ref] of imageRefsInArtifact(content)) media.set(id, ref);
172
+ };
173
+ rememberMedia(root.content);
174
+ yield {
175
+ path: root.filename,
176
+ content: root.content
177
+ };
178
+ if (includeDescendants) {
179
+ const seen = new Set([sessionId]);
180
+ const collect = async function* (nodes) {
181
+ for (const node of nodes) {
182
+ signal?.throwIfAborted();
183
+ const id = node.session.header.id;
184
+ if (seen.has(id)) continue;
185
+ seen.add(id);
186
+ await flushLiveSessionLog(deps, id, signal);
187
+ const raw = await deps.sessionPersistence.readRaw(id, signal);
188
+ signal?.throwIfAborted();
189
+ if (raw === void 0) throw new Error(`subagent "${id}" has no stored log artifact`);
190
+ rememberMedia(raw.content);
191
+ yield {
192
+ path: `subagents/${safeSessionIdSegment(id)}/${raw.filename}`,
193
+ content: raw.content
194
+ };
195
+ yield* collect(node.descendants);
196
+ }
197
+ };
198
+ const lineage = await deps.sessionQuery.traceSession(sessionId, signal);
199
+ signal?.throwIfAborted();
200
+ yield* collect(lineage.descendants);
201
+ }
202
+ for (const ref of media.values()) {
203
+ signal?.throwIfAborted();
204
+ const stored = await deps.attachments.readImage(ref, signal);
205
+ signal?.throwIfAborted();
206
+ yield {
207
+ path: mediaEntryPath(ref),
208
+ data: stored.data
209
+ };
210
+ }
211
+ }
212
+ /** How many code units of artifact text one zip push carries (bounded encode memory). */
213
+ const PUSH_CHUNK_CODE_UNITS = 65536;
214
+ /** How many bytes of media one zip push carries (bounded memory; images are already size-capped). */
215
+ const PUSH_CHUNK_BYTES = 65536;
216
+ /** Byte capacity retained by the response stream before ZIP production waits for pull. */
217
+ const RESPONSE_HIGH_WATER_MARK_BYTES = 65536;
218
+ /** One producer waiter released only when ReadableStream pull restores capacity. */
219
+ var ResponseCapacityGate = class {
220
+ releasePending;
221
+ /**
222
+ * Wait until the response queue has positive byte capacity or cancellation wins.
223
+ * @param controller - response controller whose desired size owns capacity.
224
+ * @param signal - combined request/consumer cancellation.
225
+ */
226
+ async wait(controller, signal) {
227
+ signal.throwIfAborted();
228
+ if (controller.desiredSize === null || controller.desiredSize > 0) return;
229
+ await new Promise((resolve) => {
230
+ const release = () => {
231
+ this.releasePending = void 0;
232
+ signal.removeEventListener("abort", release);
233
+ resolve();
234
+ };
235
+ this.releasePending = release;
236
+ signal.addEventListener("abort", release, { once: true });
237
+ });
238
+ signal.throwIfAborted();
239
+ }
240
+ /** Release the current producer waiter after a consumer pull. */
241
+ pulled() {
242
+ this.releasePending?.();
243
+ }
244
+ };
245
+ /**
246
+ * Push one media object's bytes into a deflate stream in bounded chunks,
247
+ * waiting for consumer capacity between chunks like the artifact path does.
248
+ * @param deflate - the zip entry's deflate stream.
249
+ * @param data - the stored image bytes.
250
+ * @param controller - response queue controller.
251
+ * @param capacity - pull-driven response-capacity gate.
252
+ * @param signal - cancellation; throws when aborted.
253
+ */
254
+ async function pushBinaryChunks(deflate, data, controller, capacity, signal) {
255
+ let offset = 0;
256
+ do {
257
+ signal.throwIfAborted();
258
+ const end = Math.min(offset + PUSH_CHUNK_BYTES, data.byteLength);
259
+ const finalChunk = end >= data.byteLength;
260
+ deflate.push(data.subarray(offset, end), finalChunk);
261
+ offset = end;
262
+ await capacity.wait(controller, signal);
263
+ } while (offset < data.byteLength);
264
+ }
265
+ /**
266
+ * Push one artifact's text into a deflate stream in bounded chunks, never
267
+ * splitting a surrogate pair across a chunk boundary (a lone high surrogate
268
+ * re-encodes as U+FFFD and would silently corrupt the exported artifact).
269
+ * @param deflate - the zip entry's deflate stream.
270
+ * @param content - the artifact text verbatim.
271
+ * @param controller - response queue controller.
272
+ * @param capacity - pull-driven response-capacity gate.
273
+ * @param signal - cancellation; throws when aborted.
274
+ */
275
+ async function pushArtifactChunks(deflate, content, controller, capacity, signal) {
276
+ const encoder = new TextEncoder();
277
+ let offset = 0;
278
+ let finalChunk;
279
+ do {
280
+ signal.throwIfAborted();
281
+ let end = Math.min(offset + PUSH_CHUNK_CODE_UNITS, content.length);
282
+ if (end < content.length && end - offset > 1) {
283
+ const last = content.charCodeAt(end - 1);
284
+ if (last >= 55296 && last <= 56319) end -= 1;
285
+ }
286
+ finalChunk = end >= content.length;
287
+ deflate.push(encoder.encode(content.slice(offset, end)), finalChunk);
288
+ offset = end;
289
+ await capacity.wait(controller, signal);
290
+ } while (!finalChunk);
291
+ }
292
+ /**
293
+ * Stream one session-log ZIP as a WHATWG ReadableStream. The root artifact is
294
+ * read and validated by the caller before this is called (missing root or
295
+ * missing services answer cleanly before any byte is produced); each entry is
296
+ * then encoded and deflated in bounded chunks as it is produced, so the
297
+ * archive bytes arrive incrementally. A descendant that fails to read errors
298
+ * the stream (fail-loud, never silent under-export).
299
+ * @param deps - the mounted export services (the caller answered 500 before this runs).
300
+ * @param root - the already-read root artifact (first zip entry).
301
+ * @param sessionId - the root session id.
302
+ * @param includeDescendants - whether to include every subagent descendant.
303
+ * @param compressionLevel - validated fflate DEFLATE level for every ZIP entry.
304
+ * @param signal - request cancellation combined with response-consumer cancellation.
305
+ * @returns the zip byte stream.
306
+ */
307
+ function streamSessionLogZip(deps, root, sessionId, includeDescendants, compressionLevel, signal) {
308
+ const consumerAbort = new AbortController();
309
+ const producerSignal = AbortSignal.any([signal, consumerAbort.signal]);
310
+ let zip;
311
+ let zipTerminated = false;
312
+ const capacity = new ResponseCapacityGate();
313
+ const terminateZip = () => {
314
+ if (zip === void 0 || zipTerminated) return;
315
+ zipTerminated = true;
316
+ zip.terminate();
317
+ };
318
+ return new ReadableStream({
319
+ start(controller) {
320
+ const archive = new Zip((error, data, final) => {
321
+ /* v8 ignore next 3 -- fflate reports only internal zip failures, unreachable for valid inputs */
322
+ if (error) {
323
+ controller.error(error);
324
+ return;
325
+ }
326
+ /* v8 ignore next -- fflate may emit empty chunks; not controllable from tests */
327
+ if (data.byteLength > 0) controller.enqueue(data);
328
+ if (final) controller.close();
329
+ });
330
+ zip = archive;
331
+ (async () => {
332
+ try {
333
+ for await (const entry of sessionLogZipEntries(deps, root, sessionId, includeDescendants, producerSignal)) {
334
+ const deflate = new ZipDeflate(entry.path, { level: compressionLevel });
335
+ archive.add(deflate);
336
+ if ("content" in entry) await pushArtifactChunks(deflate, entry.content, controller, capacity, producerSignal);
337
+ else await pushBinaryChunks(deflate, entry.data, controller, capacity, producerSignal);
338
+ }
339
+ archive.end();
340
+ } catch (error) {
341
+ /* v8 ignore next -- typed backends reject with Error, and DOMException is one in Node */
342
+ terminateZip();
343
+ controller.error(error instanceof Error ? error : new Error(String(error)));
344
+ }
345
+ })();
346
+ },
347
+ pull() {
348
+ capacity.pulled();
349
+ },
350
+ cancel(reason) {
351
+ consumerAbort.abort(reason instanceof Error ? reason : /* @__PURE__ */ new Error("session log export stream cancelled"));
352
+ terminateZip();
353
+ }
354
+ }, {
355
+ highWaterMark: RESPONSE_HIGH_WATER_MARK_BYTES,
356
+ size: (chunk) => chunk.byteLength
357
+ });
358
+ }
359
+ //#endregion
1
360
  //#region lib/types/index.js
2
- /** Web Session-log download command over the host endpoint owned by ApiProxy. */
361
+ /** Session-log download command and Host-owned streaming route. */
3
362
  const name = "session-log-download";
4
- const inject = ["commands"];
363
+ const inject = ["commands", "connection"];
364
+ /** Stable browser download path retained across the transport migration. */
365
+ const SESSION_LOG_EXPORT_PATH = "/api/session.export";
366
+ /** Validate Session-log archive configuration. */
367
+ const Config = Schema.object({ compressionLevel: Schema.number().step(1).min(0).max(9).default(6) });
5
368
  const REQUESTED = {
6
369
  kind: "success",
7
370
  text: "Session log download requested."
8
371
  };
9
372
  /**
10
- * Register the Web-only `/export` command that the browser download plugin observes.
373
+ * Register the Web-only `/export` command and authenticated ZIP download route.
11
374
  * @param ctx - Host context carrying the human-command registry.
375
+ * @param config - resolved compression policy.
12
376
  */
13
- function apply(ctx) {
377
+ function apply(ctx, config = {}) {
14
378
  ctx.effect(() => ctx.commands.register({
15
379
  name: "export",
16
380
  description: "Download this Session log as a ZIP archive",
@@ -19,6 +383,53 @@ function apply(ctx) {
19
383
  text: "The Web /export command does not accept a path."
20
384
  })
21
385
  }), "session-log-download: command");
386
+ connectionOf(ctx).fetch.register({
387
+ path: SESSION_LOG_EXPORT_PATH,
388
+ methods: ["GET", "HEAD"],
389
+ fetch: async (request) => {
390
+ const response = await sessionLogExportResponse(ctx, request, config.compressionLevel ?? 6);
391
+ if (request.method === "GET") return response;
392
+ await response.body?.cancel();
393
+ return new Response(null, {
394
+ status: response.status,
395
+ headers: response.headers
396
+ });
397
+ }
398
+ });
399
+ }
400
+ function connectionOf(ctx) {
401
+ return Reflect.get(ctx, "connection");
402
+ }
403
+ async function sessionLogExportResponse(ctx, request, compressionLevel) {
404
+ const url = new URL(request.url);
405
+ const query = Object.fromEntries(url.searchParams);
406
+ const sessionIdValue = query["sessionId"];
407
+ const descendantsValue = query["includeDescendants"];
408
+ if (sessionIdValue === void 0 || sessionIdValue.length === 0 || descendantsValue !== void 0 && descendantsValue !== "true" && descendantsValue !== "false") return new Response("missing or invalid sessionId query parameter", { status: 400 });
409
+ const sessionId = brandString(sessionIdValue);
410
+ const deps = sessionLogExportDeps(ctx);
411
+ if (deps.sessionQuery === void 0 || deps.sessionPersistence === void 0 || deps.attachments === void 0) return new Response("session log export is unavailable: missing session-query, session-persistence, or attachments service", { status: 500 });
412
+ if (!deps.sessionPersistence.supportsRawArtifacts) return new Response("session log export is unavailable: the persistence backend does not expose per-session raw artifacts", { status: 501 });
413
+ const ready = {
414
+ sessionQuery: deps.sessionQuery,
415
+ sessionPersistence: deps.sessionPersistence,
416
+ attachments: deps.attachments,
417
+ sessions: deps.sessions
418
+ };
419
+ let root;
420
+ try {
421
+ await flushLiveSessionLog(deps, sessionId, request.signal);
422
+ root = await deps.sessionPersistence.readRaw(sessionId, request.signal);
423
+ request.signal.throwIfAborted();
424
+ } catch {
425
+ request.signal.throwIfAborted();
426
+ return new Response("session log export failed to prepare the stored artifact", { status: 500 });
427
+ }
428
+ if (root === void 0) return new Response("session not found", { status: 404 });
429
+ return new Response(streamSessionLogZip(ready, root, sessionId, descendantsValue === "true", compressionLevel, request.signal), { headers: {
430
+ "content-type": "application/zip",
431
+ "content-disposition": `attachment; filename="${sessionLogZipFilename(sessionId)}"`
432
+ } });
22
433
  }
23
434
  //#endregion
24
- export { apply, inject, name };
435
+ export { Config, DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, SESSION_LOG_EXPORT_PATH, apply, flushLiveSessionLog, inject, name, sessionLogExportDeps, sessionLogZipEntries, sessionLogZipFilename, streamSessionLogZip };
package/lib/invariant.js CHANGED
@@ -3,7 +3,10 @@
3
3
  const PACKAGE_NAME = "@deepseek-ai/dsh-session-log-export";
4
4
  const name = "session-export-invariant";
5
5
  const inject = ["invariants"];
6
- /** No runtime invariant: the command registry owns lifecycle pairing and ApiProxy owns ZIP integrity. */
6
+ /**
7
+ * No runtime invariant: Connection and the command registry own both
8
+ * registrations, while each export reads authoritative Session services.
9
+ */
7
10
  const install = () => {};
8
11
  /**
9
12
  * Register this package's invariant companion.
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Host-side session-log download: streams one ZIP archive whose files are the
3
+ * sessions' stored artifact text verbatim plus every referenced media object.
4
+ * The root artifact sits under its original base name (`session.jsonl`); each
5
+ * subagent descendant under `subagents/<id>/<filename>`; each image referenced
6
+ * by any included log under `media/<attachmentId>.<ext>` (content-addressed,
7
+ * so one archive never duplicates a shared image). No manifest is written —
8
+ * every file is byte-identical to the backend's durable artifact or attachment
9
+ * store and self-describing through its own header line or media type. Before
10
+ * each live session's artifact read, the SessionStore flush barrier makes the
11
+ * current in-memory log durable; cold sessions need no barrier. Request abort
12
+ * and response-consumer cancellation share one producer signal and terminate
13
+ * the active compressor.
14
+ * Compression runs on the host with fflate's streaming Zip API, so the archive
15
+ * bytes are produced incrementally and the host never holds the whole archive
16
+ * in one buffer; production waits for consumer pull whenever the response queue
17
+ * reaches its byte high-water mark, so a slow consumer bounds accumulation to
18
+ * the fixed 64 KiB response queue plus one synchronous fflate push.
19
+ * @module
20
+ */
21
+ import type { Context } from '@deepseek-ai/cordis';
22
+ import type { AttachmentStore } from '@deepseek-ai/dsh-attachment';
23
+ import type { SessionQueryEngine } from '@deepseek-ai/dsh-session-query';
24
+ import type { SessionId, SessionStore } from '@deepseek-ai/dsh-session';
25
+ import type { SessionPersistence, SessionRawArtifact } from '@deepseek-ai/dsh-session-persistence';
26
+ /** Valid fflate DEFLATE levels accepted by session-log export. */
27
+ export type SessionLogCompressionLevel = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
28
+ /** Balanced default used when Session export configuration omits a compression level. */
29
+ export declare const DEFAULT_SESSION_LOG_COMPRESSION_LEVEL: SessionLogCompressionLevel;
30
+ /** The services a session-log export needs (the live-session store is optional). */
31
+ export interface SessionLogExportDeps {
32
+ readonly sessionQuery: SessionQueryEngine | undefined;
33
+ readonly sessionPersistence: SessionPersistence | undefined;
34
+ readonly attachments: AttachmentStore | undefined;
35
+ readonly sessions: SessionStore | undefined;
36
+ }
37
+ /** The export services narrowed to the mounted ones streaming actually reads. */
38
+ export interface SessionLogExportReady {
39
+ readonly sessionQuery: SessionQueryEngine;
40
+ readonly sessionPersistence: SessionPersistence;
41
+ readonly attachments: AttachmentStore;
42
+ readonly sessions: SessionStore | undefined;
43
+ }
44
+ /**
45
+ * Resolve the persistence, session-query, and attachment services a log export needs.
46
+ * @param ctx - the composed host context.
47
+ * @returns the export services (absent when the deployment does not mount them).
48
+ */
49
+ export declare function sessionLogExportDeps(ctx: Context): SessionLogExportDeps;
50
+ /**
51
+ * Flush one currently live session through the store's authoritative durability
52
+ * barrier immediately before its raw artifact is read. A cold or absent id has
53
+ * no in-memory work to flush.
54
+ * @param deps - export services, including the optional live-session store.
55
+ * @param id - the session whose artifact is about to be read.
56
+ * @param signal - optional cancellation observed around the flush barrier.
57
+ */
58
+ export declare function flushLiveSessionLog(deps: Pick<SessionLogExportDeps, 'sessions'>, id: SessionId, signal?: AbortSignal): Promise<void>;
59
+ /** One exported file: a stored artifact text or one referenced media object. */
60
+ export type SessionLogZipEntry = {
61
+ readonly path: string;
62
+ readonly content: string;
63
+ } | {
64
+ readonly path: string;
65
+ readonly data: Uint8Array;
66
+ };
67
+ /**
68
+ * The export archive filename for one root session.
69
+ * @param sessionId - the root session id (sanitized to one safe path segment).
70
+ * @returns the attachment filename for the session's export archive.
71
+ */
72
+ export declare function sessionLogZipFilename(sessionId: string): string;
73
+ /**
74
+ * Yield the export entries in zip order: the preloaded root artifact first,
75
+ * then every subagent descendant in lineage order (each flushed when live,
76
+ * read from the persistence backend right before it is yielded, and dropped
77
+ * after the consumer moves on), then every distinct media object referenced by any of
78
+ * the included logs (read and verified from the attachment store, one archive
79
+ * entry per attachment id). The host holds at most one descendant's artifact
80
+ * text and one media object at a time beyond the root.
81
+ * @param deps - the mounted export services (the caller answered 500 before this runs).
82
+ * @param root - the already-read root artifact (read by the caller so the
83
+ * missing-session path can answer cleanly before streaming starts).
84
+ * @param sessionId - the root session id.
85
+ * @param includeDescendants - whether to include every subagent descendant.
86
+ * @param signal - optional cancellation forwarded to lineage, persistence, and attachment reads.
87
+ * @returns the export entries in zip order.
88
+ */
89
+ export declare function sessionLogZipEntries(deps: SessionLogExportReady, root: SessionRawArtifact, sessionId: SessionId, includeDescendants: boolean, signal?: AbortSignal): AsyncGenerator<SessionLogZipEntry>;
90
+ /**
91
+ * Stream one session-log ZIP as a WHATWG ReadableStream. The root artifact is
92
+ * read and validated by the caller before this is called (missing root or
93
+ * missing services answer cleanly before any byte is produced); each entry is
94
+ * then encoded and deflated in bounded chunks as it is produced, so the
95
+ * archive bytes arrive incrementally. A descendant that fails to read errors
96
+ * the stream (fail-loud, never silent under-export).
97
+ * @param deps - the mounted export services (the caller answered 500 before this runs).
98
+ * @param root - the already-read root artifact (first zip entry).
99
+ * @param sessionId - the root session id.
100
+ * @param includeDescendants - whether to include every subagent descendant.
101
+ * @param compressionLevel - validated fflate DEFLATE level for every ZIP entry.
102
+ * @param signal - request cancellation combined with response-consumer cancellation.
103
+ * @returns the zip byte stream.
104
+ */
105
+ export declare function streamSessionLogZip(deps: SessionLogExportReady, root: SessionRawArtifact, sessionId: SessionId, includeDescendants: boolean, compressionLevel: SessionLogCompressionLevel, signal: AbortSignal): ReadableStream<Uint8Array>;
106
+ //# sourceMappingURL=archive.d.ts.map
@@ -1,4 +1,5 @@
1
- import type { ObservableSnapshot, SessionId } from '@deepseek-ai/dsh-client-runtime/client';
1
+ import type { ObservableSnapshot } from '@deepseek-ai/dsh-client-store';
2
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
2
3
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
3
4
  import type { SessionLogDownloadState } from './controller.ts';
4
5
  import { NS } from './locales.ts';
@@ -1,5 +1,6 @@
1
1
  /** Browser download state shared by the Session Header button and `/export`. */
2
- import { type SessionId, type SnapshotStore } from '@deepseek-ai/dsh-client-runtime/client';
2
+ import { type SnapshotStore } from '@deepseek-ai/dsh-client-store';
3
+ import type { SessionId } from '@deepseek-ai/dsh-session/types';
3
4
  /** Download phases presented by the shared modal. */
4
5
  export type SessionLogDownloadStatus = 'downloading' | 'success' | 'error';
5
6
  /** One Session's current download-dialog state. */
@@ -1,5 +1,5 @@
1
1
  /** Browser plugin owning Session export download state and its shared modal. */
2
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client';
2
+ import type { Context as ClientContext } from '@deepseek-ai/cordis';
3
3
  import { SessionLogDownloadController } from './controller.ts';
4
4
  import { type SessionLogDownloadKey } from './locales.ts';
5
5
  declare module '@deepseek-ai/cordis' {
@@ -2,6 +2,7 @@
2
2
  export declare const NS = "session-log-download";
3
3
  /** Simplified-Chinese Session export strings. */
4
4
  export declare const zh: {
5
+ readonly 'header.action': "Session 日志";
5
6
  readonly 'dialog.preparingTitle': "正在导出 Session";
6
7
  readonly 'dialog.preparingDescription': "正在准备包含当前 Session、子 Session 和附件的 ZIP 文件。";
7
8
  readonly 'dialog.successTitle': "Session 导出已开始下载";
@@ -1,10 +1,24 @@
1
- /** Web Session-log download command over the host endpoint owned by ApiProxy. */
1
+ /** Session-log download command and Host-owned streaming route. */
2
2
  import type { Context } from '@deepseek-ai/cordis';
3
+ import Schema from '@deepseek-ai/schemastery';
4
+ import { type SessionLogCompressionLevel } from './archive.ts';
5
+ export { DEFAULT_SESSION_LOG_COMPRESSION_LEVEL, flushLiveSessionLog, sessionLogExportDeps, sessionLogZipEntries, sessionLogZipFilename, streamSessionLogZip, } from './archive.ts';
6
+ export type { SessionLogCompressionLevel, SessionLogExportDeps, SessionLogExportReady, SessionLogZipEntry, } from './archive.ts';
3
7
  export declare const name = "session-log-download";
4
8
  export declare const inject: string[];
9
+ /** Stable browser download path retained across the transport migration. */
10
+ export declare const SESSION_LOG_EXPORT_PATH = "/api/session.export";
11
+ /** Session-log archive policy. */
12
+ export interface Config {
13
+ /** DEFLATE level for each ZIP entry. @default 6 */
14
+ readonly compressionLevel?: SessionLogCompressionLevel;
15
+ }
16
+ /** Validate Session-log archive configuration. */
17
+ export declare const Config: Schema<Config>;
5
18
  /**
6
- * Register the Web-only `/export` command that the browser download plugin observes.
19
+ * Register the Web-only `/export` command and authenticated ZIP download route.
7
20
  * @param ctx - Host context carrying the human-command registry.
21
+ * @param config - resolved compression policy.
8
22
  */
9
- export declare function apply(ctx: Context): void;
23
+ export declare function apply(ctx: Context, config?: Config): void;
10
24
  //# sourceMappingURL=index.d.ts.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@deepseek-ai/dsh-session-log-export",
3
3
  "description": "Web Session-log export command and shared download dialog",
4
- "version": "0.1.1-rc.2",
4
+ "version": "0.1.2-alpha.2",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
@@ -36,38 +36,44 @@
36
36
  "lib/types/**/*.d.ts"
37
37
  ],
38
38
  "license": "MIT",
39
+ "dependencies": {
40
+ "fflate": "^0.8.2",
41
+ "@deepseek-ai/dsh-brand": "^0.1.2-alpha.2",
42
+ "@deepseek-ai/schemastery": "^3.18.2"
43
+ },
39
44
  "peerDependencies": {
40
- "@deepseek-ai/cordis": "^4.0.1",
41
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
42
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
43
- "@deepseek-ai/dsh-client-ui-commands": "^0.1.1-rc.2",
44
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
45
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
46
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2"
45
+ "@deepseek-ai/cordis": "^4.0.2"
47
46
  },
48
47
  "devDependencies": {
49
48
  "@types/react": "~18.3.1",
50
49
  "react": "^18.2.0",
51
- "@deepseek-ai/cordis": "^4.0.1",
52
- "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
53
- "@deepseek-ai/dsh-agent": "^0.1.1-rc.2",
54
- "@deepseek-ai/dsh-client-locale": "^0.1.1-rc.2",
55
- "@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
56
- "@deepseek-ai/dsh-client-ui-commands": "^0.1.1-rc.2",
57
- "@deepseek-ai/dsh-client-ui-conversation": "^0.1.1-rc.2",
58
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
59
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
60
- "@deepseek-ai/dsh-invariants": "^0.1.1-rc.2",
61
- "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
62
- "@deepseek-ai/dsh-commands": "^0.1.1-rc.2"
50
+ "@deepseek-ai/cordis": "^4.0.2",
51
+ "@deepseek-ai/cordis-plugin-loader": "^1.0.3",
52
+ "@deepseek-ai/dsh-attachment": "^0.1.2-alpha.2",
53
+ "@deepseek-ai/dsh-agent": "^0.1.2-alpha.2",
54
+ "@deepseek-ai/dsh-client-connection": "^0.1.2-alpha.2",
55
+ "@deepseek-ai/dsh-client-ui-commands": "^0.1.2-alpha.2",
56
+ "@deepseek-ai/dsh-client-store": "^0.1.2-alpha.2",
57
+ "@deepseek-ai/dsh-client-ui-conversation": "^0.1.2-alpha.2",
58
+ "@deepseek-ai/dsh-client-ui-renderer": "^0.1.2-alpha.2",
59
+ "@deepseek-ai/dsh-client-ui-session": "^0.1.2-alpha.2",
60
+ "@deepseek-ai/dsh-client-ui-primitives": "^0.1.2-alpha.2",
61
+ "@deepseek-ai/dsh-client-locale": "^0.1.2-alpha.2",
62
+ "@deepseek-ai/dsh-commands": "^0.1.2-alpha.2",
63
+ "@deepseek-ai/dsh-client-ui-slots": "^0.1.2-alpha.2",
64
+ "@deepseek-ai/dsh-invariants": "^0.1.2-alpha.2",
65
+ "@deepseek-ai/dsh-session-persistence": "^0.1.2-alpha.2",
66
+ "@deepseek-ai/dsh-session-query": "^0.1.2-alpha.2",
67
+ "@deepseek-ai/dsh-session": "^0.1.2-alpha.2"
63
68
  },
64
69
  "dsh": {
65
70
  "client": {
66
71
  "inject": [
67
72
  "@deepseek-ai/dsh-client-locale",
68
- "@deepseek-ai/dsh-client-runtime",
69
73
  "@deepseek-ai/dsh-client-ui-commands",
70
- "@deepseek-ai/dsh-client-ui-conversation"
74
+ "@deepseek-ai/dsh-client-ui-conversation",
75
+ "@deepseek-ai/dsh-client-ui-renderer",
76
+ "@deepseek-ai/dsh-client-ui-session"
71
77
  ],
72
78
  "platform": "web"
73
79
  }