dsh-deepseek-import 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Hungry-M
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,228 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # DSH chat migration
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ Capture conversations from **Edge** on `chat.deepseek.com` (one or many), export them as JSON, then rebuild them as resumable **DeepSeek Harness** sessions.
6
+
7
+ ```
8
+ Edge extension ──export──▶ dsh-deepseek-*.json ──dsh-import.cmd──▶ ~/.dsh/sessions + workspace.json
9
+ ```
10
+
11
+ ## Install
12
+
13
+ Requires **Node.js 22+** (session compression uses built-in zstd).
14
+
15
+ ```powershell
16
+ # from npm (once published)
17
+ npm i -g dsh-deepseek-import
18
+ dsh-import --list
19
+
20
+ # or run straight from source
21
+ git clone https://github.com/Hungry-M/dsh-deepseek-import.git
22
+ cd dsh-deepseek-import
23
+ node dsh-import.mjs --list
24
+ ```
25
+
26
+ The Edge extension must be loaded separately (next section) — a browser extension cannot ship inside an npm package.
27
+
28
+ ## 1. Install the extension (once)
29
+
30
+ 1. Open `edge://extensions/` in Edge
31
+ 2. Turn on **Developer mode**
32
+ 3. Click **Load unpacked** and pick the `edge-extension` folder
33
+ 4. Pin it to the toolbar (optional)
34
+
35
+ ## 2. Capture a conversation
36
+
37
+ 1. Open any conversation on `chat.deepseek.com` and let it finish loading
38
+ 2. Open the extension; the conversation appears once it has been captured
39
+ 3. To capture more, open them one by one — the list accumulates
40
+ 4. Tick what you need → **Export selected**
41
+
42
+ > If a conversation never shows up, open it and click **Capture chat**. It tries three sources in order: the browser's local cache (IndexedDB), a screen-by-screen backfill, then only the visible screen.
43
+
44
+ ### Capturing very long conversations
45
+
46
+ DeepSeek loads long conversations page by page. The extension does two things:
47
+
48
+ 1. **Merge by message identity** — each message carries `message_id`; the extension unions them by id, so scrolling up or capturing twice only ever *adds* messages, never overwrites what was already captured.
49
+ 2. **Capture chat** reads `deepseek-chat / history-message` straight from IndexedDB first. For most conversations that is the whole history in one read, with no scrolling at all.
50
+
51
+ ## 3. Import into DSH
52
+
53
+ Double-click **`dsh-import.cmd`** in the workspace (or drop JSON files onto it). With no arguments it uses the newest `dsh-deepseek-*.json` found in your download folder.
54
+
55
+ - Lists your DSH workspaces; pick one by number
56
+ - Each conversation becomes one session: title, order and body (including code blocks and tables) are preserved
57
+ - **Attachments** (images, txt, pdf, docx…) that can be downloaded are saved under `<workspace>/.dsh-imported/<sessionId>/` with their original names; anything that cannot be downloaded degrades to `[附件未下载:name (size) 原链接]`
58
+ - **Thinking** goes to `thinking.md` in the same folder (never injected into the context)
59
+ - **Edited / regenerated versions**: only the selected branch stays in the body, all versions go to `revisions.md`
60
+ - **Interrupted outputs** keep only a numbered placeholder (`[中断输出 #27:未产生完整正文…]`); add `--keep-interrupted` to keep the partial text
61
+ - `workspace.json` is backed up before it is modified
62
+ - Everything is mirrored to `import.log`
63
+
64
+ ### Important: restart DSH after importing
65
+
66
+ The sidebar list comes from `~/.dsh/storages/workspace.json`, which DSH **only reads at startup**. Quit DSH completely and reopen it.
67
+
68
+ If the session still does not show up, use this order instead: **quit DSH → run `dsh-import.cmd` → start DSH**. A running DSH can overwrite `workspace.json` with its in-memory list.
69
+
70
+ ### Configuration (shared by CLI / TUI / extension)
71
+
72
+ `import.config.json`, next to this file:
73
+
74
+ ```json
75
+ { "downloadDir": "D:\\Downloads", "subdir": "dsh-import", "lang": "zh" }
76
+ ```
77
+
78
+ | Key | Meaning |
79
+ |---|---|
80
+ | `downloadDir` | Where to look for exports (defaults to the real system download folder) |
81
+ | `subdir` | Subfolder convention: the extension saves to `<downloadDir>/<subdir>/`, the importer scans it too |
82
+ | `lang` | `zh` / `en`, affects the TUI and the extension UI |
83
+ | `autoTrim` | Default `true`: auto-trim conversations above the threshold |
84
+ | `autoTrimChars` | Trigger threshold, default `120000` chars |
85
+ | `autoTrimTurns` | Max turns kept, default `20` |
86
+ | `autoTrimBudget` | Char budget for the kept part, default `60000` (walks backwards from the end) |
87
+
88
+ The same four auto-trim values live in the extension's ⚙ settings (checkbox + a collapsed "Advanced" section). They travel inside the export's `options`, and the importer resolves them in this order:
89
+
90
+ ```
91
+ CLI flags > export options (extension settings) > import.config.json > built-in defaults
92
+ ```
93
+
94
+ Sidecar location is also selectable in the extension (**Write sidecars to DSH home**), which equals `--no-sidecar`.
95
+
96
+ ### CLI (for AI / scripts)
97
+
98
+ ```powershell
99
+ node dsh-import.mjs --list # list workspaces
100
+ node dsh-import.mjs # newest export in the download folder
101
+ node dsh-import.mjs export.json -w "D:\proj" # explicit workspace
102
+ node dsh-import.mjs export.json --dry-run # parse only
103
+ node dsh-import.mjs export.json --thinking # also inline thinking (default: thinking.md only)
104
+ node dsh-import.mjs export.json --json # machine-readable: {ok, workspace, created[]}
105
+ node dsh-import.mjs export.json --keep-versions # keep pre-edit versions (default: current branch only)
106
+ node dsh-import.mjs export.json --keep-interrupted # keep partial interrupted output
107
+ node dsh-import.mjs export.json --tail 20 # only the last 20 turns
108
+ node dsh-import.mjs export.json --no-auto-trim # disable auto trimming
109
+ node dsh-import.mjs export.json --no-sidecar # sidecars to ~/.dsh/import-archive/
110
+ node dsh-import.mjs export.json --sidecar-dir "E:\arch"
111
+ ```
112
+
113
+ `dsh-import.mjs` is also a module:
114
+
115
+ ```js
116
+ import { runImport, getWorkspaces, listExports, peekExport } from './dsh-import.mjs';
117
+ await runImport({ files: ['x.json'], workspacePath: 'D:\\proj', only: ['<conversation id>'] });
118
+ ```
119
+
120
+ ### Interactive UI
121
+
122
+ ```powershell
123
+ node dsh-import-tui.mjs
124
+ ```
125
+
126
+ Pick an export (size/time shown) → preview conversations (messages/chars/thinking/attachments) → select (`all`, `1,3`, `1-2`) → **pick a branch point** → pick workspace → confirm.
127
+
128
+ ### Branching from a specific message
129
+
130
+ To start a session at a chosen point instead of "the last N turns":
131
+
132
+ ```powershell
133
+ node dsh-import.mjs export.json --from-match "some text you typed"
134
+ node dsh-import.mjs export.json --from-index 157
135
+ ```
136
+
137
+ ### Auditing imported sessions
138
+
139
+ DSH session format v4 requires the first model-visible node to be a `system/message`.
140
+ Imports from `0.1.0` predate that fix and would break the moment you kept chatting in them.
141
+ `0.1.1` writes the head on import, and this command repairs sessions created earlier:
142
+
143
+ ```powershell
144
+ node dsh-import.mjs --audit # report only
145
+ node dsh-import.mjs --repair # insert/move the head, resequence, drop the projection cache
146
+ ```
147
+
148
+ - `--from-match` does a case-insensitive substring match over **user messages**, first hit wins
149
+ - If the same text was edited and appears several times, the TUI lists them chronologically and you pick by number
150
+ - Choosing a branch point disables auto-trim (your choice wins); the full text still goes to `transcript.md`
151
+
152
+ ### How sidecar files are found later
153
+
154
+ The first user message ends with an index line:
155
+
156
+ ```
157
+ [导入存档(需要时可直接读取):思考 2 段 → .dsh-imported/<sessionId>/thinking.md;附件 → …]
158
+ ```
159
+
160
+ Those files sit inside the workspace, so the model can read them on demand with ordinary file tools (nothing is pushed into the context automatically).
161
+
162
+ ## Don't import very long conversations in one piece
163
+
164
+ A 227-message / 520k-char conversation is roughly 250–350k tokens. DSH compacts automatically (`dsh-compaction-basic`), but after compaction only ~16% of recent history stays verbatim and the rest becomes a summary — the import is effectively wasted.
165
+
166
+ ```powershell
167
+ node dsh-import.mjs export.json --tail 20
168
+ ```
169
+
170
+ - Only the last 20 turns are imported; the full text goes to `.dsh-imported/<sessionId>/transcript.md`
171
+ - The first message carries a note plus the sidecar index, so the model can grep the transcript when it needs old details
172
+ - Conversations over 150k chars always get a `transcript.md`, even without `--tail`
173
+ - `transcript.md` starts with an index (`#N role chars — first line`), and each section is headed `## #N`, so `grep "## #123"` jumps straight to a message
174
+
175
+ ## FAQ
176
+
177
+ **Double-clicking the cmd closes instantly?**
178
+ Older builds had LF line endings, which `cmd.exe` mis-parses. Current builds use CRLF, always stop at `pause`, and write `import.log`.
179
+
180
+ **"Node.js not found"?**
181
+ Lookup order: `node-path.txt` → PATH → `Program Files\nodejs` → `Program Files (x86)\nodejs` → `%LOCALAPPDATA%\Programs\nodejs` → `%APPDATA%\npm` → `node.exe` next to the script.
182
+
183
+ **Which file does the cmd import?**
184
+ Without arguments: the newest `dsh-deepseek-*.json` in the download folder. You can also drop files/folders onto it.
185
+
186
+ **Body is only a fragment?**
187
+ Open the conversation and click **Capture chat**; then check the "来源" (source) tag in the list.
188
+
189
+ ## Known limitations
190
+
191
+ - **Thinking is archived, not inlined**: DSH reasoning blocks carry provider signatures; faking them would poison later requests. Read `thinking.md`, or force inline with `--thinking`.
192
+ - **Attachments are not added to DSH's attachment store**: they are written to disk and referenced by path. When no download URL is available only the name and size survive.
193
+ - **Web-search citations**: the cache record has no `references` table, so `[reference:N]` markers stay as plain text with no links.
194
+ - **Only text and attachments** are migrated; tool calls and search results have no structured export.
195
+ - **Session format is DSH internals** (v4 JSONL + zstd, multi-frame). A future DSH change may require an update here.
196
+ - The extension relies on DeepSeek's own responses; if a redesign breaks capture, use **Capture chat** as the fallback.
197
+
198
+ ## 4. Moving an imported session to another workspace
199
+
200
+ ```powershell
201
+ node move-session.mjs --list # sessions per workspace
202
+ node move-session.mjs <session-id> --to "D:\other" # move
203
+ node move-session.mjs <session-id> --to "D:\other" --dry-run # preview
204
+ ```
205
+
206
+ It updates four things: `cwd` in the session log, the project folder that holds the session, `sessionIds` in `workspace.json`, and the `.dsh-imported/<id>` folder. Restart DSH afterwards.
207
+
208
+ ## Rollback
209
+
210
+ - Delete a session folder: `~/.dsh/sessions/--<workspace>--/session-<uuid>/`
211
+ - Restore the list: copy the matching `workspace.json.bak-*` over `workspace.json`
212
+ - Single session: `node remove-session.mjs <session-id>`
213
+
214
+ ## Layout
215
+
216
+ ```
217
+ edge-extension/
218
+ manifest.json MV3 manifest
219
+ merge.js window merging by message id / overlap
220
+ inject.js main world: read-only sniffing of fetch/XHR responses
221
+ stream-parser.js streaming patch protocol parser
222
+ content.js isolated world: normalize, cache reader, DOM fallback
223
+ background.js capture storage, attachment fetching
224
+ popup.html/js/css selection and export UI
225
+ dsh-import.mjs export JSON → DSH sessions
226
+ dsh-import-tui.mjs interactive importer
227
+ dsh-import.cmd double-click entry
228
+ ```
package/README.zh.md ADDED
@@ -0,0 +1,262 @@
1
+ # DSH 对话迁移
2
+
3
+ [English](README.md) | 中文
4
+
5
+ 从 **Edge** 抓取 `chat.deepseek.com` 的对话(单个或多个),导出 JSON,再由本地脚本重建为 DeepSeek Harness 的可继续会话。
6
+
7
+ ```
8
+ Edge 扩展 ──导出──▶ dsh-deepseek-*.json ──dsh-import.cmd──▶ ~/.dsh/sessions + workspace.json
9
+ ```
10
+
11
+ ## 安装
12
+
13
+ 需要 **Node.js 22+**(会话压缩用了内置 zstd)。
14
+
15
+ ```powershell
16
+ # 从 npm(发布后)
17
+ npm i -g dsh-deepseek-import
18
+ dsh-import --list
19
+
20
+ # 或直接用源码,不装全局
21
+ git clone https://github.com/Hungry-M/dsh-deepseek-import.git
22
+ cd dsh-deepseek-import
23
+ node dsh-import.mjs --list
24
+ ```
25
+
26
+ 扩展必须单独加载(见下一节)——浏览器扩展没法随 npm 包一起装。
27
+
28
+ ## 一、安装扩展(一次性)
29
+
30
+ 1. Edge 地址栏打开 `edge://extensions/`
31
+ 2. 打开左下角 **开发人员模式**
32
+ 3. 点 **加载解压缩的扩展**,选择本目录下的 `edge-extension` 文件夹
33
+ 4. 固定到工具栏(可选)
34
+
35
+ ## 二、抓取对话
36
+
37
+ 1. 在 Edge 打开 `chat.deepseek.com` 的任意对话,**等它加载完**(消息出现即可)。
38
+ 2. 点扩展图标,列表里会出现这条对话(标题 + 消息条数)。
39
+ 3. 想看多条:逐个打开对话,每次都会自动捕获,列表累积。
40
+ 4. 勾选要导出的对话 → **导出选中** → 保存 JSON。
41
+
42
+ > 原理:扩展在页面主世界给 `fetch`/XHR 挂了只读嗅探,只读取 DeepSeek 自己的响应体,不发额外请求、不上传任何东西。
43
+ > 如果某条对话没被捕获,打开它然后点扩展的 **抓取对话**(会按「本地缓存 → 逐屏回溯 → 当前屏」依次尝试)。
44
+
45
+ ### 超长对话怎么抓全
46
+
47
+ DeepSeek 的长对话是**分页加载**的,每次只能看到一屏。扩展做了两件事:
48
+
49
+ 1. **按消息身份合并**:每页消息带 `message_id`,扩展把它当唯一键取并集,再按 id 排序。所以往上翻、往下翻、重复抓同一页,都只会**累加**,不会覆盖已经拿到的部分。
50
+ 2. **抓取对话**:一个按钮搞定三种来源——先读浏览器本地缓存(IndexedDB 里有全量),读不到再逐屏向上回溯,最后退回只抓当前屏。
51
+
52
+ > 为什么 DOM 只抓到一屏也不怕:滚动时的分页请求会被扩展嗅探到,那份数据是带 id 的完整消息;DOM 抓取只是补充。
53
+
54
+ ## 三、导入到 DSH
55
+
56
+ 双击工作区里的 **`dsh-import.cmd`**,或把 JSON 拖到它上面。
57
+
58
+ - 会列出 DSH 里的工作区,选序号决定导入到哪个工作区
59
+ - 每个对话重建为一个会话,标题、顺序、正文(含代码块/表格等 Markdown)原样保留
60
+ - **附件**(图片、txt、pdf、docx…)能下载的落盘到 `<工作区>/.dsh-imported/<sessionId>/`,保留原文件名;下载不到时退化成 `[附件未下载:文件名(大小) 原链接]`,由你本机匹配
61
+ - **思考过程**写进同目录的 `thinking.md`(旁挂存档,不进入对话上下文)
62
+ - **编辑/重新生成留下的多个版本**:正文只保留当前分支,全部版本写进同目录的 `revisions.md`
63
+ - 改动 `workspace.json` 前自动备份为 `workspace.json.bak-<时间>`
64
+ - 所有输出同时写进 `import.log`,窗口一闪而过也能查原因
65
+
66
+ ### 重要:导入后要重启 DSH
67
+
68
+ 侧边栏列表读自 `~/.dsh/storages/workspace.json`,DSH **只在启动时加载**。导入完成后请**完全退出并重新打开** DeepSeek Harness,否则新会话不会出现。
69
+
70
+ 如果重启后还是看不到,顺序改成:**先完全退出 DSH → 再双击 `dsh-import.cmd` → 再启动 DSH**。因为 DSH 运行时可能用内存里的旧列表覆盖 `workspace.json`。
71
+
72
+ ### 配置(CLI / TUI / 扩展共用一套约定)
73
+
74
+ `import.config.json`(与本文件同目录):
75
+
76
+ ```json
77
+ { "downloadDir": "D:\\Downloads", "subdir": "dsh-import", "lang": "zh" }
78
+ ```
79
+
80
+ | 字段 | 作用 |
81
+ |---|---|
82
+ | `downloadDir` | 去哪个目录找导出文件(默认读系统真实下载目录) |
83
+ | `subdir` | 下载子目录约定:扩展存到 `<下载目录>/<subdir>/`,CLI/TUI 也扫这里 |
84
+ | `lang` | `zh` / `en`,影响 TUI 与扩展界面 |
85
+ | `autoTrim` | 默认 `true`:超过阈值的长对话自动截断 |
86
+ | `autoTrimChars` | 触发阈值,默认 `120000` 字 |
87
+ | `autoTrimTurns` | 最多保留轮数,默认 `20` |
88
+ | `autoTrimBudget` | 保留部分的字符预算,默认 `60000` 字(从最后一条往前累加到超预算) |
89
+
90
+ `--no-auto-trim` 可单次关闭;`--tail N` 显式指定保留轮数。
91
+
92
+ 旁挂文件默认写在 `<工作区>/.dsh-imported/`,可以改位置:`--no-sidecar`(写到 `~/.dsh/import-archive/`)或 `--sidecar-dir <路径>`。
93
+
94
+ 旁挂文件的位置也能在设置里选:勾 **旁挂文件写到 DSH 目录** 就不往工作区写 .dsh-imported(等价于 CLI 的 `--no-sidecar`)。
95
+
96
+ 这四个参数也可以在**扩展的 ⚙ 设置**里改:勾选框控制开关,下面的「高级参数」是默认折叠的,里面是三个数字。扩展会把它们写进导出 JSON 的 `options`,导入时按这个优先级生效:
97
+
98
+ ```
99
+ 命令行参数 > 导出文件里的 options(扩展设置) > import.config.json > 内置默认值
100
+ ```
101
+
102
+ ```powershell
103
+ node dsh-import.mjs --set-dir "E:\chat" # 写入配置
104
+ node dsh-import.mjs --dir "E:\chat" # 只本次生效,不改配置
105
+ ```
106
+
107
+ 扩展侧:面板右上 ⚙ → **保存子目录**(留空=直接存下载目录)。浏览器不允许扩展写任意绝对路径,所以「共享」靠的是这个子目录约定,而不是写死某个盘符。
108
+
109
+ ### 命令行接口(给 AI / 脚本用)
110
+
111
+ ```powershell
112
+ node dsh-import.mjs --list # 只列工作区
113
+ node dsh-import.mjs # 自动用下载目录里最新的导出
114
+ node dsh-import.mjs 导出.json -w "D:\proj" # 直接指定工作区
115
+ node dsh-import.mjs 导出.json --dry-run # 只解析不写盘
116
+ node dsh-import.mjs 导出.json --thinking # 另外把思考塞进正文(默认只写 thinking.md)
117
+ node dsh-import.mjs 导出.json --json # 机器可读输出:{ok, workspace, created[]}
118
+ node dsh-import.mjs 导出.json --keep-versions # 保留编辑前的旧版本(默认只留当前分支)
119
+ node dsh-import.mjs 导出.json --keep-interrupted # 保留不完整的中断输出(默认只留编号占位符)
120
+ node dsh-import.mjs 导出.json --only "<对话 id>" # 只导指定对话(逗号分隔可多个)
121
+ node dsh-import.mjs 导出.json --from-match "某句话" # 从匹配到的用户消息开始分支
122
+ node dsh-import.mjs 导出.json --from-index 157 # 按消息序号分支
123
+ node dsh-import.mjs --audit # 体检已导入会话(system 头是否完好)
124
+ node dsh-import.mjs --repair # 批量修掉有隐患的会话
125
+
126
+ > --tail 与 --from-index/--from-match 互斥(一个是取最后 N 轮,一个是从指定位置开始),同时给出会直接报错退出;--from-* 只能用于单个对话。
127
+ ```
128
+
129
+ ### 中断输出怎么处理
130
+
131
+ 被手动中止的回答,默认在会话里只留一行带编号的占位符:
132
+
133
+ ```
134
+ [中断输出 #2:未产生完整正文,思考过程见 thinking.md]
135
+ ```
136
+
137
+ - 编号来自 `message_id`,用来证明「这里确实有过一次中断输出」
138
+ - 思考照常进 `thinking.md`
139
+ - 加 `--keep-interrupted` 才保留原始的不完整正文;正文本来就空的仍然用占位符
140
+ - 识别依据:消息的 `status` 不是 `FINISHED`、或 `incomplete_message` 为真、或正文为空;老数据没有 `status` 时只能靠「正文为空」判断
141
+
142
+ ### 从某条消息开始分支
143
+
144
+ 想把会话“截断”在某个位置重新开始,而不是只用最后 N 轮:
145
+
146
+ ```powershell
147
+ node dsh-import.mjs 导出.json --from-match "照你的顺序来吧"
148
+ node dsh-import.mjs 导出.json --from-index 157
149
+ ```
150
+
151
+ - `--from-match` 在**用户消息**里做不区分大小写的全文匹配,命中第一条
152
+ - 同一句话被编辑过、出现多次时,TUI 会把它们按时间顺序编号列出来让你选:
153
+ ```
154
+ 1 #158 照你的顺序来吧,只要你不忘记清单。…
155
+ 2 #160 照你的顺序来吧,只要你不忘记清单。…
156
+ 选择序号(回车=1,共 2 条匹配):
157
+ ```
158
+ - 选了分支点就不再自动截断(尊重你的选择),完整原文照样写进 `transcript.md`
159
+
160
+ ### 超长对话别整段导入
161
+
162
+ 一份 227 条、52 万字符的对话大约 25–35 万 tokens,直接导入必然超过上下文窗口。DSH 会自动压缩(`dsh-compaction-basic`),但压缩后只逐字保留约 16% 的近期内容,其余变成摘要——等于白导。
163
+
164
+ ```powershell
165
+ node dsh-import.mjs 导出.json --tail 20
166
+ ```
167
+
168
+ - 只导入**最后 20 轮**,全文另存 `.dsh-imported/<sessionId>/transcript.md`
169
+ - 第一条消息里会附一行说明 + 旁挂文件索引,模型需要早期细节时可以自己 grep 那个文件
170
+ - 超过 15 万字符的对话即使不加 `--tail` 也会自动留一份 `transcript.md`
171
+
172
+ `dsh-import.mjs` 同时是模块,可被引用:```js
173
+ import { runImport, getWorkspaces, listExports, peekExport } from './dsh-import.mjs';
174
+ await runImport({ files: ['x.json'], workspacePath: 'D:\\proj', only: ['<对话 id>'] });
175
+ ```
176
+
177
+ ### 交互界面
178
+
179
+ ```powershell
180
+ node dsh-import-tui.mjs
181
+ ```
182
+
183
+ 菜单流程:选导出文件(带大小/时间)→ 预览对话(条数/字数/思考段数/附件数)→ 多选(`all`、`1,3`、`1-2`)→ 选工作区 → 确认导入。
184
+
185
+ ### 旁挂文件怎么被读到
186
+
187
+ 导入时会在**第一条用户消息末尾**附一行索引:
188
+
189
+ ```
190
+ [导入存档(需要时可直接读取):思考 2 段 → .dsh-imported/<sessionId>/thinking.md;附件 → .dsh-imported/<sessionId>/]
191
+ ```
192
+
193
+ 这些文件都在工作区目录内,后续对话里模型可以随时用文件工具按需读取(不会自动塞进上下文)。
194
+
195
+ ## 常见问题
196
+
197
+ **复制工作区后 DSH 列表全空?**
198
+ DSH 要求 **Session ID 全局唯一**,同一个 ID 出现在两个项目目录会让整个会话列表加载失败(`duplicate JSONL session id`)。`copy-workspace.mjs` 现在会为每条复制出的会话**重新生成 UUID**,并同步改写 header 与事件里的引用。改完先体检再启动:
199
+
200
+ ```powershell
201
+ node check-sessions.mjs
202
+ ```
203
+
204
+ 它会检查重复 ID、目录名与 header.id 是否一致、每帧能否解压、seq 是否连续、workspace.json 与磁盘是否对账。退出码 0 才启动 DSH。
205
+
206
+ 如果已经出了重复,把冲突的项目目录整体移出 `~/.dsh/sessions`(DSH 只扫这一层),再从 workspace.json 移除对应条目,重启即可恢复——原会话文件不会损坏。
207
+
208
+ **双击 cmd 提示「找不到 Node.js」?**
209
+ 脚本按这个顺序找:`node-path.txt`(同目录,可手写一行路径)→ PATH → `Program Files\nodejs` → `Program Files (x86)\nodejs` → `%LOCALAPPDATA%\Programs\nodejs` → `%APPDATA%\npm` → 同目录的 `node.exe`。都没命中就装一个 Node 22+,或把 `node.exe` 放到脚本旁边。
210
+
211
+ **cmd 到底会导入哪个文件?**
212
+ 不带参数:`~/Downloads` 里**最新**的 `dsh-deepseek-*.json`。也可以把文件(或整个文件夹)拖到 cmd 上,或自己传多个路径。工作区是运行时从 `workspace.json` 列出来让你选序号的。
213
+
214
+ **测试时要不要删掉 Downloads 里的旧 json?**
215
+ 不用。脚本只取最新一份,旧的不会混进来。想批量导入就拖多个文件上去,或传一个目录(目录里所有 `.json` 都会被读)。
216
+
217
+ **双击 cmd,窗口一闪而过什么都没发生?**
218
+ 旧版本的文件用了 LF 换行,`cmd.exe` 解析失败,脚本根本没执行。现在的版本改用 CRLF,并且无论成败都会停在 `pause`,输出同时写入 `import.log`。如果仍然闪退,直接看 `import.log`。
219
+
220
+ **导入成功但侧边栏没有?**
221
+ 先按上面「重要」那段重启;还不行就按「先退出 → 再导入 → 再启动」的顺序来。
222
+
223
+ **正文只抓到一小段?**
224
+ 说明命中的不是完整历史接口。打开 DeepSeek 页面 → 点扩展的 **诊断** → 会导出 `dsh-debug-*.json`(含页面请求的 URL、content-type、非 JSON 响应样本)。把它发给我即可定位真实接口。注意:文件里有会话 id 和 URL,介意可自行打码。
225
+
226
+ ## 已知限制
227
+
228
+ - **思考过程旁挂**:写进 `thinking.md`,不进上下文。DSH 的 reasoning 块带 provider 签名,硬塞正文会污染后续请求;想看时直接翻文件,或加 `--thinking` 强制内联。
229
+ - **附件不进 DSH 附件库**:DSH 的图片是内容寻址附件(`~/.dsh/attachments/v1/objects/...`),伪造引用有损坏历史的风险;这里改为落盘 + 路径引用,模型可以用工具读。拿不到下载地址时只留文件名与大小。
230
+ - **正文完整性**:历史接口 `/api/v0/chat/history_messages` 返回的正文可能偏短;回答生成时的 `/api/v0/chat/completion` 是 `text/event-stream` 补丁流(`THINK` 片段=思考,其余片段=正文),扩展会边读边拼,生成完即写入捕获。所以**新回答**是完整的;很久以前的老回答以历史接口返回的内容为准。
231
+ - **只迁移文本与附件**:DeepSeek 工具调用、联网搜索的结果没有结构化导出,只能作为正文的一部分。
232
+ - **会话格式是 DSH 内部实现**(v4 JSONL + zstd,多帧拼接)。DSH 升级若改格式,本工具需要跟着改;导入前建议确认版本。
233
+ - 扩展依赖 DeepSeek 页面自己的接口响应;页面大改版导致捕获不到时,用 **抓取对话** 兜底。
234
+
235
+ ## 四、把导入的会话换到别的工作区
236
+
237
+ ```powershell
238
+ node move-session.mjs --list # 看每个工作区有哪些会话
239
+ node move-session.mjs <session-id> --to "D:\某个工作区" # 换过去
240
+ node move-session.mjs <session-id> --to "D:\某个工作区" --dry-run # 先看要动什么
241
+ ```
242
+
243
+ 同时改四处:会话日志里的 `cwd`、会话目录所在的项目文件夹、`workspace.json` 的 `sessionIds`、`.dsh-imported/<id>` 里的附件。改完重启 DSH 生效。
244
+
245
+ ## 回滚
246
+
247
+ - 删除会话目录:`~/.dsh/sessions/--<工作区>--/session-<uuid>/`
248
+ - 恢复列表:用同名的 `workspace.json.bak-*` 覆盖 `workspace.json`
249
+ - 单个会话:`node remove-session.mjs <session-id>`
250
+
251
+ ## 目录结构
252
+
253
+ ```
254
+ edge-extension/
255
+ manifest.json MV3 清单
256
+ inject.js 主世界:只读嗅探 fetch/XHR 响应
257
+ content.js 隔离世界:归一化、DOM 兜底、图片转 dataURL
258
+ background.js 存储捕获结果
259
+ popup.html/js/css 选择与导出界面
260
+ dsh-import.mjs 导出 JSON → DSH 会话
261
+ dsh-import.cmd 双击入口
262
+ ```
@@ -0,0 +1,60 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import os from 'node:os';
4
+
5
+ const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
6
+ const SESSIONS = path.join(DSH_HOME, 'sessions');
7
+ const STORE = path.join(DSH_HOME, 'storages', 'workspace.json');
8
+ const ARCHIVE = path.join(DSH_HOME, 'import-archive');
9
+
10
+ const store = JSON.parse(fs.readFileSync(STORE, 'utf8').replace(/^\uFEFF/, ''));
11
+ const workspaces = Object.entries(store.tables?.workspaces ?? {}).map(([id, ws]) => ({
12
+ id,
13
+ path: ws.path,
14
+ title: ws.title,
15
+ sessionIds: ws.sessionIds || [],
16
+ }));
17
+
18
+ const registered = new Set(workspaces.flatMap((w) => w.sessionIds));
19
+ const onDisk = new Map();
20
+ if (fs.existsSync(SESSIONS)) {
21
+ for (const project of fs.readdirSync(SESSIONS)) {
22
+ const dir = path.join(SESSIONS, project);
23
+ if (!fs.statSync(dir).isDirectory()) continue;
24
+ for (const entry of fs.readdirSync(dir)) {
25
+ if (!entry.startsWith('session-')) continue;
26
+ onDisk.set(entry, path.join(dir, entry));
27
+ }
28
+ }
29
+ }
30
+
31
+ console.log('DSH_HOME:', DSH_HOME);
32
+ console.log('工作区:', workspaces.length, '| 已登记会话:', registered.size, '| 磁盘会话目录:', onDisk.size);
33
+
34
+ const missingDirs = [...registered].filter((id) => !onDisk.has(id));
35
+ const unregistered = [...onDisk.keys()].filter((id) => !registered.has(id));
36
+ console.log('\n登记了但目录不存在:', missingDirs.length);
37
+ missingDirs.slice(0, 10).forEach((id) => console.log(' -', id));
38
+ console.log('目录存在但没登记:', unregistered.length);
39
+ unregistered.slice(0, 10).forEach((id) => console.log(' -', id));
40
+
41
+ console.log('\n--- 工作区里的 .dsh-imported ---');
42
+ for (const ws of workspaces) {
43
+ if (!ws.path) continue;
44
+ const root = path.join(ws.path, '.dsh-imported');
45
+ if (!fs.existsSync(root)) continue;
46
+ const entries = fs.readdirSync(root).filter((n) => n !== '.gitignore');
47
+ const orphans = entries.filter((n) => !registered.has(n));
48
+ console.log(`${ws.title}: ${entries.length} 个目录,其中未登记 ${orphans.length} 个`);
49
+ for (const o of orphans.slice(0, 10)) console.log(' orphan:', o);
50
+ }
51
+
52
+ console.log('\n--- ~/.dsh/import-archive ---');
53
+ if (fs.existsSync(ARCHIVE)) {
54
+ const entries = fs.readdirSync(ARCHIVE);
55
+ const orphans = entries.filter((n) => !registered.has(n));
56
+ console.log(`${entries.length} 个目录,其中未登记 ${orphans.length} 个`);
57
+ for (const o of orphans.slice(0, 10)) console.log(' orphan:', o);
58
+ } else {
59
+ console.log('不存在');
60
+ }