dsh-deepseek-import 0.1.0 → 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/README.md +11 -0
- package/README.zh.md +2 -0
- package/dsh-import.mjs +27 -0
- package/package.json +4 -2
- package/repair-session.mjs +220 -0
package/README.md
CHANGED
|
@@ -134,6 +134,17 @@ node dsh-import.mjs export.json --from-match "some text you typed"
|
|
|
134
134
|
node dsh-import.mjs export.json --from-index 157
|
|
135
135
|
```
|
|
136
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
|
+
|
|
137
148
|
- `--from-match` does a case-insensitive substring match over **user messages**, first hit wins
|
|
138
149
|
- If the same text was edited and appears several times, the TUI lists them chronologically and you pick by number
|
|
139
150
|
- Choosing a branch point disables auto-trim (your choice wins); the full text still goes to `transcript.md`
|
package/README.zh.md
CHANGED
|
@@ -120,6 +120,8 @@ node dsh-import.mjs 导出.json --keep-interrupted # 保留不完整的中断输
|
|
|
120
120
|
node dsh-import.mjs 导出.json --only "<对话 id>" # 只导指定对话(逗号分隔可多个)
|
|
121
121
|
node dsh-import.mjs 导出.json --from-match "某句话" # 从匹配到的用户消息开始分支
|
|
122
122
|
node dsh-import.mjs 导出.json --from-index 157 # 按消息序号分支
|
|
123
|
+
node dsh-import.mjs --audit # 体检已导入会话(system 头是否完好)
|
|
124
|
+
node dsh-import.mjs --repair # 批量修掉有隐患的会话
|
|
123
125
|
|
|
124
126
|
> --tail 与 --from-index/--from-match 互斥(一个是取最后 N 轮,一个是从指定位置开始),同时给出会直接报错退出;--from-* 只能用于单个对话。
|
|
125
127
|
```
|
package/dsh-import.mjs
CHANGED
|
@@ -85,6 +85,9 @@ function parseArgs(argv) {
|
|
|
85
85
|
else if (arg === '--keep-interrupted') opts.keepInterrupted = true;
|
|
86
86
|
else if (arg === '--tail') opts.tail = Number(argv[++i]);
|
|
87
87
|
else if (arg === '--no-auto-trim') opts.autoTrim = false;
|
|
88
|
+
else if (arg === '--no-system-head') opts.noSystemHead = true;
|
|
89
|
+
else if (arg === '--audit') opts.audit = true;
|
|
90
|
+
else if (arg === '--repair') { opts.audit = true; opts.applyRepair = true; }
|
|
88
91
|
else if (arg === '--no-sidecar' || arg === '--sidecar') opts.noSidecar = true;
|
|
89
92
|
else if (arg === '--sidecar-dir') opts.sidecarDir = argv[++i];
|
|
90
93
|
else if (arg === '--context-window') opts.contextWindow = Number(argv[++i]) || null;
|
|
@@ -362,6 +365,9 @@ function conversationsOf(file) {
|
|
|
362
365
|
|
|
363
366
|
// ---------------------------------------------------------------- 生成会话
|
|
364
367
|
|
|
368
|
+
const IMPORT_SYSTEM_NOTE =
|
|
369
|
+
'本会话的历史来自 DeepSeek 网页对话的导入,以下消息为用户与助手的原始记录,顺序与原文一致。';
|
|
370
|
+
|
|
365
371
|
const DATA_URL = /^data:([^;,]+);base64,(.*)$/s;
|
|
366
372
|
const MIME_EXT = {
|
|
367
373
|
'image/png': 'png',
|
|
@@ -706,6 +712,21 @@ function buildEvents(conversation, workspacePath, sessionId) {
|
|
|
706
712
|
});
|
|
707
713
|
}
|
|
708
714
|
push('step/start', { turn, step });
|
|
715
|
+
// v4 不变量:surface 的第 0 个节点必须是受保护的 system 头。
|
|
716
|
+
// 导入的历史前面没有 system/message 时,会话能打开,但一旦继续对话、
|
|
717
|
+
// 运行时追加自己的 system/message 就会被校验器判为损坏。
|
|
718
|
+
if (turn === 1 && !opts.noSystemHead) {
|
|
719
|
+
push('system/message', {
|
|
720
|
+
turn,
|
|
721
|
+
step,
|
|
722
|
+
message: {
|
|
723
|
+
role: 'system',
|
|
724
|
+
content: [{ type: 'text', text: IMPORT_SYSTEM_NOTE }],
|
|
725
|
+
source: { kind: 'system-prompt' },
|
|
726
|
+
id: crypto.randomUUID(),
|
|
727
|
+
},
|
|
728
|
+
}, { surfaceOp: 'append' });
|
|
729
|
+
}
|
|
709
730
|
const seqNow = seq;
|
|
710
731
|
// 只在第一条用户消息尾部附上存档索引
|
|
711
732
|
if (sidecarNote && firstUserSeq === undefined) text += sidecarNote;
|
|
@@ -884,6 +905,12 @@ function writeTranscript(conversation, workspacePath, sessionId) {
|
|
|
884
905
|
// ---------------------------------------------------------------- 主流程
|
|
885
906
|
|
|
886
907
|
async function main() {
|
|
908
|
+
if (opts.audit) {
|
|
909
|
+
const { auditSessions } = await import('./repair-session.mjs');
|
|
910
|
+
auditSessions({ dryRun: opts.dryRun, apply: opts.applyRepair === true });
|
|
911
|
+
return { audited: true };
|
|
912
|
+
}
|
|
913
|
+
|
|
887
914
|
// ---------- 参数自检:互相排斥的组合直接拒绝 ----------
|
|
888
915
|
if (opts.tail !== undefined && opts.tail !== null) {
|
|
889
916
|
if (!Number.isInteger(opts.tail) || opts.tail < 1) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-deepseek-import",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "DeepSeek web chat → DeepSeek Harness sessions (Edge extension + importer CLI)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"dsh-import.cmd",
|
|
15
15
|
"move-session.mjs",
|
|
16
16
|
"remove-session.mjs",
|
|
17
|
+
"repair-session.mjs",
|
|
17
18
|
"audit-imports.mjs",
|
|
18
19
|
"edge-extension/",
|
|
19
20
|
"README.md",
|
|
@@ -39,6 +40,7 @@
|
|
|
39
40
|
},
|
|
40
41
|
"scripts": {
|
|
41
42
|
"list": "node dsh-import.mjs --list",
|
|
42
|
-
"tui": "node dsh-import-tui.mjs"
|
|
43
|
+
"tui": "node dsh-import-tui.mjs",
|
|
44
|
+
"audit": "node repair-session.mjs --audit"
|
|
43
45
|
}
|
|
44
46
|
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* repair-session.mjs — 给导入的会话补上 v4 要求的「受保护 system 头」
|
|
4
|
+
*
|
|
5
|
+
* 背景:DSH 会话格式 v4 要求 surface 的第 0 个节点是 system/message。
|
|
6
|
+
* 早期的导入器没写这一条,会话能打开,但一旦继续对话、运行时追加自己的
|
|
7
|
+
* system/message,就会报 `system/message requires a protected first surface head`。
|
|
8
|
+
*
|
|
9
|
+
* 用法:
|
|
10
|
+
* node repair-session.mjs --audit # 体检所有会话
|
|
11
|
+
* node repair-session.mjs <session-dir> --dry-run
|
|
12
|
+
* node repair-session.mjs <session-dir>
|
|
13
|
+
* node repair-session.mjs --all [--dry-run] # 批量修所有需要修的
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import os from 'node:os';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import zlib from 'node:zlib';
|
|
20
|
+
import crypto from 'node:crypto';
|
|
21
|
+
import { fileURLToPath } from 'node:url';
|
|
22
|
+
|
|
23
|
+
const DSH_HOME = process.env.DSH_HOME || path.join(os.homedir(), '.dsh');
|
|
24
|
+
const SESSIONS = path.join(DSH_HOME, 'sessions');
|
|
25
|
+
const CACHE = path.join(DSH_HOME, 'storages', 'session_projcache', 'sessions');
|
|
26
|
+
|
|
27
|
+
const NOTE =
|
|
28
|
+
'本会话的历史来自 DeepSeek 网页对话的导入,以下消息为用户与助手的原始记录,顺序与原文一致。';
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const dryRun = args.includes('--dry-run');
|
|
32
|
+
const audit = args.includes('--audit');
|
|
33
|
+
const all = args.includes('--all');
|
|
34
|
+
const targets = args.filter((a) => !a.startsWith('--'));
|
|
35
|
+
|
|
36
|
+
function readFrames(file) {
|
|
37
|
+
const buf = fs.readFileSync(file);
|
|
38
|
+
const magic = Buffer.from([0x28, 0xb5, 0x2f, 0xfd]);
|
|
39
|
+
const offsets = [];
|
|
40
|
+
for (let i = -1; (i = buf.indexOf(magic, i + 1)) !== -1;) offsets.push(i);
|
|
41
|
+
const texts = [];
|
|
42
|
+
for (let k = 0; k < offsets.length; k++) {
|
|
43
|
+
texts.push(zlib.zstdDecompressSync(buf.subarray(offsets[k], offsets[k + 1] ?? buf.length)).toString('utf8'));
|
|
44
|
+
}
|
|
45
|
+
return texts;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeFrames(file, texts) {
|
|
49
|
+
const encoded = Buffer.concat(texts.map((text) => zlib.zstdCompressSync(Buffer.from(text, 'utf8'))));
|
|
50
|
+
fs.writeFileSync(file, encoded);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sessionFiles() {
|
|
54
|
+
const out = [];
|
|
55
|
+
if (!fs.existsSync(SESSIONS)) return out;
|
|
56
|
+
for (const project of fs.readdirSync(SESSIONS)) {
|
|
57
|
+
const dir = path.join(SESSIONS, project);
|
|
58
|
+
if (!fs.statSync(dir).isDirectory()) continue;
|
|
59
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
60
|
+
if (!entry.startsWith('session-')) continue;
|
|
61
|
+
const file = path.join(dir, entry, 'session.v4.jsonl.zstd');
|
|
62
|
+
if (fs.existsSync(file)) out.push({ id: entry, file });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 返回 { status, header, events, firstSurfaceIndex, systemIndexes } */
|
|
69
|
+
function inspect(file) {
|
|
70
|
+
const texts = readFrames(file);
|
|
71
|
+
const rows = texts.join('').split('\n').filter(Boolean).map((line) => JSON.parse(line));
|
|
72
|
+
const header = rows[0];
|
|
73
|
+
const events = rows.slice(1);
|
|
74
|
+
const systemIndexes = events.map((e, i) => (e.type === 'system/message' ? i : -1)).filter((i) => i >= 0);
|
|
75
|
+
const surfaceTypes = new Set(['user/message', 'assistant/message', 'system/message', 'tool/result', 'developer/message']);
|
|
76
|
+
const firstSurfaceIndex = events.findIndex((e) => surfaceTypes.has(e.type));
|
|
77
|
+
return { texts, header, events, systemIndexes, firstSurfaceIndex };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function makeSystemEvent(turn, step) {
|
|
81
|
+
return {
|
|
82
|
+
type: 'system/message',
|
|
83
|
+
seq: 0,
|
|
84
|
+
time: Date.now(),
|
|
85
|
+
data: {
|
|
86
|
+
turn,
|
|
87
|
+
step,
|
|
88
|
+
message: {
|
|
89
|
+
role: 'system',
|
|
90
|
+
content: [{ type: 'text', text: NOTE }],
|
|
91
|
+
source: { kind: 'system-prompt' },
|
|
92
|
+
id: crypto.randomUUID(),
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
surfaceOp: 'append',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function planRepair({ events, systemIndexes, firstSurfaceIndex }) {
|
|
100
|
+
if (firstSurfaceIndex < 0) return { action: 'skip', reason: '没有 surface 事件' };
|
|
101
|
+
if (systemIndexes.length && systemIndexes[0] === firstSurfaceIndex) {
|
|
102
|
+
return { action: 'ok' };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const copy = events.map((e) => ({ ...e }));
|
|
106
|
+
if (systemIndexes.length) {
|
|
107
|
+
// 已有 system 但在后面:把它挪到 surface 最前面
|
|
108
|
+
const source = copy[systemIndexes[0]];
|
|
109
|
+
copy.splice(systemIndexes[0], 1);
|
|
110
|
+
const target = firstSurfaceIndex;
|
|
111
|
+
copy.splice(target, 0, {
|
|
112
|
+
...source,
|
|
113
|
+
data: { ...source.data, turn: 1, step: 1 },
|
|
114
|
+
surfaceOp: 'append',
|
|
115
|
+
});
|
|
116
|
+
return { action: 'move', events: copy };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 完全没有 system:插到第一个 surface 事件之前
|
|
120
|
+
copy.splice(firstSurfaceIndex, 0, makeSystemEvent(1, 1));
|
|
121
|
+
return { action: 'insert', events: copy };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function resequence(events) {
|
|
125
|
+
const mapping = new Map();
|
|
126
|
+
events.forEach((event, index) => mapping.set(event.seq, index));
|
|
127
|
+
const next = events.map((event, index) => ({ ...event, seq: index }));
|
|
128
|
+
// 改写对 seq 的引用(session/title 的 messageSeqs)
|
|
129
|
+
for (const event of next) {
|
|
130
|
+
if (event.type === 'session/title' && Array.isArray(event.data?.messageSeqs)) {
|
|
131
|
+
event.data = {
|
|
132
|
+
...event.data,
|
|
133
|
+
messageSeqs: event.data.messageSeqs.map((seq) => mapping.get(seq) ?? seq),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return next;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function sessionDirOf(file) {
|
|
141
|
+
return path.dirname(file);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function repair(file, plan) {
|
|
145
|
+
const { texts, header } = inspect(file);
|
|
146
|
+
const events = resequence(plan.events);
|
|
147
|
+
const lines = [JSON.stringify(header), ...events.map((e) => JSON.stringify(e))];
|
|
148
|
+
writeFrames(file, lines.map((line) => `${line}\n`));
|
|
149
|
+
|
|
150
|
+
const id = path.basename(sessionDirOf(file));
|
|
151
|
+
const cached = path.join(CACHE, `${id}.json`);
|
|
152
|
+
let cacheNote = '';
|
|
153
|
+
if (fs.existsSync(cached)) {
|
|
154
|
+
fs.rmSync(cached, { force: true });
|
|
155
|
+
cacheNote = ',投影缓存已清除';
|
|
156
|
+
}
|
|
157
|
+
return `${plan.action === 'insert' ? '已插入 system 头' : '已把 system 头移到最前'}(${events.length} 条事件${cacheNote})`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function auditSessions({ dryRun = false, apply = false } = {}) {
|
|
161
|
+
const list = sessionFiles();
|
|
162
|
+
const problems = [];
|
|
163
|
+
for (const item of list) {
|
|
164
|
+
let info;
|
|
165
|
+
try {
|
|
166
|
+
info = inspect(item.file);
|
|
167
|
+
} catch (error) {
|
|
168
|
+
problems.push({ ...item, status: `读取失败:${error.message}` });
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const plan = planRepair(info);
|
|
172
|
+
if (plan.action === 'ok') continue;
|
|
173
|
+
problems.push({ ...item, status: plan.action, plan });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
console.log(`会话总数 ${list.length},需要处理 ${problems.length}`);
|
|
177
|
+
for (const item of problems) {
|
|
178
|
+
console.log(` ${item.id} ${item.status}`);
|
|
179
|
+
if (dryRun || !apply || !item.plan) continue;
|
|
180
|
+
console.log(` → ${repair(item.file, item.plan)}`);
|
|
181
|
+
}
|
|
182
|
+
if (dryRun) console.log('\n[dry-run] 没有修改任何文件');
|
|
183
|
+
return problems;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function repairSessions(paths, { dryRun = false } = {}) {
|
|
187
|
+
for (const target of paths) {
|
|
188
|
+
const dir = target.endsWith('session.v4.jsonl.zstd') ? path.dirname(target) : target;
|
|
189
|
+
const file = path.join(dir, 'session.v4.jsonl.zstd');
|
|
190
|
+
if (!fs.existsSync(file)) {
|
|
191
|
+
console.error(`找不到 ${file}`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const info = inspect(file);
|
|
195
|
+
const plan = planRepair(info);
|
|
196
|
+
console.log(`${path.basename(dir)}: ${plan.action}`);
|
|
197
|
+
if (plan.action === 'ok' || plan.action === 'skip' || dryRun) continue;
|
|
198
|
+
console.log(' → ' + repair(file, plan));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// ---------------------------------------------------------------- 入口
|
|
203
|
+
|
|
204
|
+
const invokedDirectly =
|
|
205
|
+
process.argv[1] && path.resolve(process.argv[1]) === path.resolve(fileURLToPath(import.meta.url));
|
|
206
|
+
|
|
207
|
+
if (invokedDirectly) {
|
|
208
|
+
if (audit || all) {
|
|
209
|
+
auditSessions({ dryRun, apply: all });
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (!targets.length) {
|
|
214
|
+
console.error('用法:node repair-session.mjs --audit | <session-dir> [--dry-run] | --all');
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
repairSessions(targets, { dryRun });
|
|
219
|
+
}
|
|
220
|
+
|