@xiaoyuyu6420/dsh-backup 0.14.0 → 0.14.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.en.md +2 -1
- package/README.md +2 -1
- package/lib/client.js +8 -8
- package/lib/index.js +171 -55
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -3701,17 +3701,23 @@ export function apply(ctx, pluginConfig) {
|
|
|
3701
3701
|
};
|
|
3702
3702
|
}
|
|
3703
3703
|
|
|
3704
|
-
// ---------------- 外部聊天记录导入
|
|
3705
|
-
//
|
|
3706
|
-
//
|
|
3707
|
-
//
|
|
3704
|
+
// ---------------- 外部聊天记录导入(多来源) ----------------
|
|
3705
|
+
// 把其它 AI 工具的会话日志翻译成 DSH v4 会话日志(新建会话、绝不改原数据)。
|
|
3706
|
+
// 支持:claude-code(~/.claude/projects/*/*.jsonl)、codex(~/.codex/sessions/**/rollout-*.jsonl)。
|
|
3707
|
+
// 只映射「用户/助手文本 + 推理摘要 + 工具调用/结果」;其余事件跳过并计数。
|
|
3708
|
+
// 行格式对齐宿主:{type,seq,time,data} + 消息事件带 surfaceOp:'append'。
|
|
3708
3709
|
const CLAUDE_PROJECTS_DIR = '.claude/projects';
|
|
3710
|
+
const CODEX_SESSIONS_DIR = '.codex/sessions';
|
|
3709
3711
|
const CHAT_IMPORT_MAX_MESSAGES = 5000;
|
|
3712
|
+
const CHAT_SOURCE_LABELS = { 'claude-code': 'Claude Code', codex: 'Codex' };
|
|
3710
3713
|
|
|
3711
|
-
function
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3714
|
+
function chatSourceOf(id) {
|
|
3715
|
+
return String(id || '').startsWith('codex-') ? 'codex' : 'claude-code';
|
|
3716
|
+
}
|
|
3717
|
+
|
|
3718
|
+
function chatSessionId(prefix, name) {
|
|
3719
|
+
const tail = String(name).replace(/[^A-Za-z0-9]/g, '').slice(-12);
|
|
3720
|
+
return `${prefix}-${tail || 'session'}`;
|
|
3715
3721
|
}
|
|
3716
3722
|
|
|
3717
3723
|
async function listClaudeSessions() {
|
|
@@ -3729,12 +3735,39 @@ export function apply(ctx, pluginConfig) {
|
|
|
3729
3735
|
const abs = `${root}/${proj.name}/${f}`;
|
|
3730
3736
|
let st;
|
|
3731
3737
|
try { st = await fsStat(abs); } catch { continue; }
|
|
3732
|
-
out.push({ abs, rel: `${CLAUDE_PROJECTS_DIR}/${proj.name}/${f}`,
|
|
3738
|
+
out.push({ abs, rel: `${CLAUDE_PROJECTS_DIR}/${proj.name}/${f}`, project: proj.name, file: f, id: chatSessionId('claude', `${proj.name}${f}`), bytes: st.size, mtime: st.mtimeMs });
|
|
3733
3739
|
}
|
|
3734
3740
|
}
|
|
3735
3741
|
return out;
|
|
3736
3742
|
}
|
|
3737
3743
|
|
|
3744
|
+
async function listCodexSessions() {
|
|
3745
|
+
const { home } = paths();
|
|
3746
|
+
const root = `${home}/${CODEX_SESSIONS_DIR}`;
|
|
3747
|
+
const out = [];
|
|
3748
|
+
const walk = async (dir, depth) => {
|
|
3749
|
+
if (depth > 6 || out.length >= 200) return;
|
|
3750
|
+
let entries;
|
|
3751
|
+
try { entries = await readdir(dir, { withFileTypes: true }); } catch { return; }
|
|
3752
|
+
for (const e of entries) {
|
|
3753
|
+
const abs = `${dir}/${e.name}`;
|
|
3754
|
+
if (e.isDirectory()) { await walk(abs, depth + 1); continue; }
|
|
3755
|
+
if (!e.name.endsWith('.jsonl')) continue;
|
|
3756
|
+
let st;
|
|
3757
|
+
try { st = await fsStat(abs); } catch { continue; }
|
|
3758
|
+
const stem = e.name.replace(/^rollout-/, '').replace(/\.jsonl$/, '');
|
|
3759
|
+
out.push({ abs, rel: abs.slice(home.length + 1), project: stem.slice(0, 10), file: e.name, id: chatSessionId('codex', stem), bytes: st.size, mtime: st.mtimeMs });
|
|
3760
|
+
}
|
|
3761
|
+
};
|
|
3762
|
+
await walk(root, 0);
|
|
3763
|
+
return out;
|
|
3764
|
+
}
|
|
3765
|
+
|
|
3766
|
+
function textBlocks(blocks) {
|
|
3767
|
+
if (!Array.isArray(blocks)) return [];
|
|
3768
|
+
return blocks.filter((b) => b && typeof b.text === 'string' && b.text).map((b) => b.text);
|
|
3769
|
+
}
|
|
3770
|
+
|
|
3738
3771
|
/** 解析 Claude Code JSONL → 归一化消息序列。 */
|
|
3739
3772
|
async function parseClaudeSession(abs) {
|
|
3740
3773
|
const text = await readFile(abs, 'utf8');
|
|
@@ -3751,7 +3784,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
3751
3784
|
const blocks = Array.isArray(msg.content)
|
|
3752
3785
|
? msg.content
|
|
3753
3786
|
: (typeof msg.content === 'string' ? [{ type: 'text', text: msg.content }] : []);
|
|
3754
|
-
const time = rec.timestamp ? (Date.parse(rec.timestamp) ||
|
|
3787
|
+
const time = rec.timestamp ? (Date.parse(rec.timestamp) || 0) : 0;
|
|
3755
3788
|
if (type === 'user') {
|
|
3756
3789
|
const texts = [];
|
|
3757
3790
|
const results = [];
|
|
@@ -3774,7 +3807,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
3774
3807
|
for (const b of blocks) {
|
|
3775
3808
|
if (b && b.type === 'text' && typeof b.text === 'string') texts.push(b.text);
|
|
3776
3809
|
else if (b && b.type === 'thinking' && typeof b.thinking === 'string') reasoning.push(b.thinking);
|
|
3777
|
-
else if (b && b.type === 'tool_use') toolCalls.push({ id: String(b.id || `call-${toolCalls.length}`), name: String(b.name || 'tool'),
|
|
3810
|
+
else if (b && b.type === 'tool_use') toolCalls.push({ id: String(b.id || `call-${toolCalls.length}`), name: String(b.name || 'tool'), arguments: JSON.stringify(b.input ?? {}) });
|
|
3778
3811
|
else skipped += 1;
|
|
3779
3812
|
}
|
|
3780
3813
|
if (texts.length || reasoning.length || toolCalls.length) {
|
|
@@ -3782,11 +3815,72 @@ export function apply(ctx, pluginConfig) {
|
|
|
3782
3815
|
}
|
|
3783
3816
|
}
|
|
3784
3817
|
}
|
|
3785
|
-
return { messages, skipped };
|
|
3818
|
+
return { messages, skipped, cwd: null, model: null };
|
|
3786
3819
|
}
|
|
3787
3820
|
|
|
3788
|
-
/**
|
|
3789
|
-
function
|
|
3821
|
+
/** 解析 Codex rollout JSONL → 归一化消息序列(函数调用与结果按 call_id 配对)。 */
|
|
3822
|
+
async function parseCodexSession(abs) {
|
|
3823
|
+
const text = await readFile(abs, 'utf8');
|
|
3824
|
+
const messages = [];
|
|
3825
|
+
const calls = new Map();
|
|
3826
|
+
let skipped = 0;
|
|
3827
|
+
let cwd = null;
|
|
3828
|
+
let model = null;
|
|
3829
|
+
for (const line of text.split('\n')) {
|
|
3830
|
+
if (!line.trim()) continue;
|
|
3831
|
+
if (messages.length >= CHAT_IMPORT_MAX_MESSAGES) break;
|
|
3832
|
+
let rec;
|
|
3833
|
+
try { rec = JSON.parse(line); } catch { skipped += 1; continue; }
|
|
3834
|
+
const p = rec.payload || {};
|
|
3835
|
+
if (rec.type === 'session_meta') {
|
|
3836
|
+
if (typeof p.cwd === 'string') cwd = p.cwd;
|
|
3837
|
+
continue;
|
|
3838
|
+
}
|
|
3839
|
+
if (rec.type === 'turn_context') {
|
|
3840
|
+
if (typeof p.model === 'string') model = p.model;
|
|
3841
|
+
continue;
|
|
3842
|
+
}
|
|
3843
|
+
if (rec.type !== 'response_item') { skipped += 1; continue; }
|
|
3844
|
+
const time = rec.timestamp ? (Date.parse(rec.timestamp) || 0) : 0;
|
|
3845
|
+
const kind = p.type;
|
|
3846
|
+
if (kind === 'message') {
|
|
3847
|
+
if (p.role === 'user') messages.push({ role: 'user', texts: textBlocks(p.content), reasoning: [], toolCalls: [], time });
|
|
3848
|
+
else if (p.role === 'assistant') messages.push({ role: 'assistant', texts: textBlocks(p.content), reasoning: [], toolCalls: [], time });
|
|
3849
|
+
else skipped += 1; // developer / system 指令不导入
|
|
3850
|
+
} else if (kind === 'reasoning') {
|
|
3851
|
+
const summary = textBlocks(p.summary).join('\n').trim();
|
|
3852
|
+
if (summary) messages.push({ role: 'assistant', texts: [], reasoning: [summary], toolCalls: [], time });
|
|
3853
|
+
else skipped += 1; // 只有加密推理,无法还原
|
|
3854
|
+
} else if (kind === 'function_call' || kind === 'custom_tool_call') {
|
|
3855
|
+
const callId = String(p.call_id || p.id || '');
|
|
3856
|
+
const args = typeof p.arguments === 'string'
|
|
3857
|
+
? p.arguments
|
|
3858
|
+
: (typeof p.input === 'string' ? JSON.stringify({ input: p.input }) : '{}');
|
|
3859
|
+
calls.set(callId, { name: String(p.name || 'tool'), arguments: args });
|
|
3860
|
+
} else if (kind === 'function_call_output' || kind === 'custom_tool_call_output') {
|
|
3861
|
+
const callId = String(p.call_id || '');
|
|
3862
|
+
const call = calls.get(callId);
|
|
3863
|
+
const body = typeof p.output === 'string' ? p.output : textBlocks(p.output).join('\n');
|
|
3864
|
+
messages.push({
|
|
3865
|
+
role: 'tool',
|
|
3866
|
+
texts: [body],
|
|
3867
|
+
reasoning: [],
|
|
3868
|
+
toolCalls: [],
|
|
3869
|
+
callId,
|
|
3870
|
+
name: call?.name,
|
|
3871
|
+
arguments: call?.arguments,
|
|
3872
|
+
time,
|
|
3873
|
+
});
|
|
3874
|
+
} else {
|
|
3875
|
+
skipped += 1; // compaction / agent_message / world_state 等
|
|
3876
|
+
}
|
|
3877
|
+
}
|
|
3878
|
+
const kept = messages.filter((m) => m.role === 'tool' ? m.texts.length > 0 : (m.texts.length > 0 || m.reasoning.length > 0));
|
|
3879
|
+
return { messages: kept, skipped, cwd, model };
|
|
3880
|
+
}
|
|
3881
|
+
|
|
3882
|
+
/** 归一化消息 → DSH v4 事件行(seq 连续;消息事件带 surfaceOp)。 */
|
|
3883
|
+
function chatMessagesToEvents(messages, baseTime) {
|
|
3790
3884
|
const rows = [];
|
|
3791
3885
|
let seq = 0;
|
|
3792
3886
|
let turn = 0;
|
|
@@ -3803,6 +3897,8 @@ export function apply(ctx, pluginConfig) {
|
|
|
3803
3897
|
if (m.role === 'user') {
|
|
3804
3898
|
push('user/message', { id: `u${turn}`, role: 'user', content: m.texts.map((t) => ({ type: 'text', text: t })), source: { kind: 'user' } }, true);
|
|
3805
3899
|
} else if (m.role === 'tool') {
|
|
3900
|
+
// Codex 的调用与结果分两条记录:同一回合内先补 tool/call,再落 tool/result。
|
|
3901
|
+
if (m.name) push('tool/call', { turn, step: 1, callId: m.callId, name: m.name, arguments: m.arguments || '{}' });
|
|
3806
3902
|
push('tool/result', {
|
|
3807
3903
|
turn,
|
|
3808
3904
|
step: 1,
|
|
@@ -3819,11 +3915,17 @@ export function apply(ctx, pluginConfig) {
|
|
|
3819
3915
|
const content = [];
|
|
3820
3916
|
for (const r of m.reasoning) content.push({ type: 'reasoning', text: r });
|
|
3821
3917
|
for (const t of m.texts) content.push({ type: 'text', text: t });
|
|
3822
|
-
for (const c of m.toolCalls) content.push({ type: 'tool-call', id: c.id, name: c.name, arguments:
|
|
3918
|
+
for (const c of m.toolCalls) content.push({ type: 'tool-call', id: c.id, name: c.name, arguments: c.arguments });
|
|
3919
|
+
if (!content.length) {
|
|
3920
|
+
rows.pop();
|
|
3921
|
+
rows.pop();
|
|
3922
|
+
turn -= 1;
|
|
3923
|
+
continue;
|
|
3924
|
+
}
|
|
3823
3925
|
push('assistant/message', {
|
|
3824
3926
|
turn,
|
|
3825
3927
|
step: 1,
|
|
3826
|
-
message: { id: `a${turn}`, role: 'assistant', content, source: { kind: 'model', provider: '
|
|
3928
|
+
message: { id: `a${turn}`, role: 'assistant', content, source: { kind: 'model', provider: 'imported', model: 'imported' } },
|
|
3827
3929
|
stream: [],
|
|
3828
3930
|
}, true);
|
|
3829
3931
|
}
|
|
@@ -3833,61 +3935,74 @@ export function apply(ctx, pluginConfig) {
|
|
|
3833
3935
|
return rows;
|
|
3834
3936
|
}
|
|
3835
3937
|
|
|
3836
|
-
/**
|
|
3938
|
+
/** 外部聊天来源汇总(检测 + 会话清单)。 */
|
|
3837
3939
|
async function chatSources() {
|
|
3838
|
-
const
|
|
3940
|
+
const [claude, codex] = await Promise.all([listClaudeSessions(), listCodexSessions()]);
|
|
3839
3941
|
const sessions = [];
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3942
|
+
const sources = [];
|
|
3943
|
+
const collect = async (sourceId, list) => {
|
|
3944
|
+
sources.push({ id: sourceId, label: CHAT_SOURCE_LABELS[sourceId], detected: list.length > 0, count: list.length });
|
|
3945
|
+
for (const s of list.slice(0, 50)) {
|
|
3946
|
+
try {
|
|
3947
|
+
const parsed = sourceId === 'codex' ? await parseCodexSession(s.abs) : await parseClaudeSession(s.abs);
|
|
3948
|
+
const firstUser = parsed.messages.find((m) => m.role === 'user');
|
|
3949
|
+
sessions.push({
|
|
3950
|
+
source: sourceId,
|
|
3951
|
+
id: s.id,
|
|
3952
|
+
file: s.file,
|
|
3953
|
+
project: s.project,
|
|
3954
|
+
messages: parsed.messages.length,
|
|
3955
|
+
title: firstUser ? String(firstUser.texts[0] || '').slice(0, 60) : '',
|
|
3956
|
+
});
|
|
3957
|
+
} catch { /* 单文件解析失败不影响其它会话 */ }
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
await collect('claude-code', claude);
|
|
3961
|
+
await collect('codex', codex);
|
|
3962
|
+
const detected = sources.filter((s) => s.detected);
|
|
3853
3963
|
return {
|
|
3854
3964
|
ok: true,
|
|
3855
|
-
|
|
3856
|
-
detected:
|
|
3965
|
+
sources,
|
|
3966
|
+
detected: detected.length > 0,
|
|
3857
3967
|
sessions,
|
|
3858
|
-
summary:
|
|
3968
|
+
summary: detected.length
|
|
3969
|
+
? `💬 检测到 ${detected.map((s) => `${s.label} ${s.count}`).join(' · ')} 个会话`
|
|
3970
|
+
: '💬 没有检测到可导入的聊天记录(Claude Code / Codex)',
|
|
3859
3971
|
};
|
|
3860
3972
|
}
|
|
3861
3973
|
|
|
3862
|
-
async function
|
|
3863
|
-
const
|
|
3974
|
+
async function buildChatImport(source, sessionId) {
|
|
3975
|
+
const src = source === 'codex' ? 'codex' : 'claude-code';
|
|
3976
|
+
const list = src === 'codex' ? await listCodexSessions() : await listClaudeSessions();
|
|
3864
3977
|
const target = list.find((s) => s.id === sessionId || s.file === sessionId || s.abs === sessionId);
|
|
3865
|
-
if (!target) throw new Error(`找不到
|
|
3866
|
-
const parsed = await parseClaudeSession(target.abs);
|
|
3978
|
+
if (!target) throw new Error(`找不到 ${CHAT_SOURCE_LABELS[src]} 会话:${sessionId}`);
|
|
3979
|
+
const parsed = src === 'codex' ? await parseCodexSession(target.abs) : await parseClaudeSession(target.abs);
|
|
3867
3980
|
if (!parsed.messages.length) throw new Error('该会话没有可导入的文本消息');
|
|
3868
3981
|
// 宿主要求 time 是安全整数:毫秒时间戳必须取整(mtimeMs 可能是小数)。
|
|
3869
|
-
const rows =
|
|
3982
|
+
const rows = chatMessagesToEvents(parsed.messages, Math.floor(target.mtime || Date.now()));
|
|
3870
3983
|
const id = `${target.id}-${stampNow()}`;
|
|
3871
|
-
const header = { type: 'session', version: 4, id, createdAt: Math.floor(target.mtime || Date.now()), isSeeded: false, delegationDepth: 0 };
|
|
3984
|
+
const header = { type: 'session', version: 4, id, createdAt: Math.floor(target.mtime || Date.now()), isSeeded: false, delegationDepth: 0, ...(parsed.cwd ? { cwd: parsed.cwd } : {}) };
|
|
3872
3985
|
const lines = [JSON.stringify(header), ...rows.map((r) => JSON.stringify(r))];
|
|
3873
3986
|
const defect = validateSessionLines(lines);
|
|
3874
|
-
if (defect !== null) throw new Error(
|
|
3875
|
-
return { target, parsed, rows, header, id, lines };
|
|
3987
|
+
if (defect !== null) throw new Error(`导入会话未通过宿主容器校验,已中止:${defect}`);
|
|
3988
|
+
return { source: src, target, parsed, rows, header, id, lines };
|
|
3876
3989
|
}
|
|
3877
3990
|
|
|
3878
|
-
/**
|
|
3991
|
+
/** 预览:只读,不写盘。 */
|
|
3879
3992
|
async function chatPreview(opts, signal) {
|
|
3880
3993
|
const sessionId = typeof opts?.session === 'string' ? opts.session : '';
|
|
3881
3994
|
if (!sessionId) throw new Error('缺少会话 id');
|
|
3882
3995
|
if (signal?.aborted) throw new Error('操作已取消');
|
|
3883
|
-
const
|
|
3996
|
+
const source = typeof opts?.source === 'string' && opts.source ? opts.source : chatSourceOf(sessionId);
|
|
3997
|
+
const bundle = await buildChatImport(source, sessionId);
|
|
3884
3998
|
const userCount = bundle.parsed.messages.filter((m) => m.role === 'user').length;
|
|
3885
3999
|
const asstCount = bundle.parsed.messages.filter((m) => m.role === 'assistant').length;
|
|
3886
4000
|
const toolCount = bundle.parsed.messages.filter((m) => m.role === 'tool').length;
|
|
4001
|
+
const label = CHAT_SOURCE_LABELS[bundle.source];
|
|
3887
4002
|
return {
|
|
3888
4003
|
ok: true,
|
|
3889
4004
|
dryRun: true,
|
|
3890
|
-
source:
|
|
4005
|
+
source: bundle.source,
|
|
3891
4006
|
session: bundle.target.id,
|
|
3892
4007
|
file: bundle.target.file,
|
|
3893
4008
|
messages: bundle.parsed.messages.length,
|
|
@@ -3897,45 +4012,46 @@ export function apply(ctx, pluginConfig) {
|
|
|
3897
4012
|
skipped: bundle.parsed.skipped,
|
|
3898
4013
|
events: bundle.rows.length,
|
|
3899
4014
|
newId: bundle.id,
|
|
3900
|
-
warnings: ['
|
|
3901
|
-
summary: `💬
|
|
4015
|
+
warnings: ['仅导入文本、推理摘要与工具调用/结果;图片/文件块与私有事件已跳过', '导入为新的 DSH 会话,不改动原会话'],
|
|
4016
|
+
summary: `💬 ${label} 导入预览\n${bundle.parsed.messages.length} 条消息(用户 ${userCount} / 助手 ${asstCount} / 工具结果 ${toolCount})→ ${bundle.rows.length} 个事件\n跳过 ${bundle.parsed.skipped} 个非文本事件;导入为新会话,原会话不动`,
|
|
3902
4017
|
};
|
|
3903
4018
|
}
|
|
3904
4019
|
|
|
3905
|
-
/**
|
|
4020
|
+
/** 执行导入:默认 dry-run;confirm:true 才写(新会话目录,绝不覆盖)。 */
|
|
3906
4021
|
async function chatApply(opts, signal) {
|
|
3907
4022
|
const sessionId = typeof opts?.session === 'string' ? opts.session : '';
|
|
3908
4023
|
if (!sessionId) throw new Error('缺少会话 id');
|
|
3909
4024
|
if (opts?.confirm !== true) return chatPreview(opts, signal);
|
|
3910
|
-
const
|
|
4025
|
+
const source = typeof opts?.source === 'string' && opts.source ? opts.source : chatSourceOf(sessionId);
|
|
4026
|
+
const bundle = await buildChatImport(source, sessionId);
|
|
3911
4027
|
const { dshHome } = paths();
|
|
3912
4028
|
const dir = `${dshHome}/sessions/_no-cwd/${encodeSessionSegment(bundle.id)}`;
|
|
3913
|
-
if (pathExists(dir)) throw new Error('
|
|
4029
|
+
if (pathExists(dir)) throw new Error('目标会话目录已存在,拒绝覆盖');
|
|
3914
4030
|
await mkdir(dir, { recursive: true });
|
|
3915
4031
|
const headerFrame = zlib.zstdCompressSync(Buffer.from(`${JSON.stringify(bundle.header)}\n`));
|
|
3916
4032
|
const bodyFrame = zlib.zstdCompressSync(Buffer.from(`${bundle.rows.map((r) => JSON.stringify(r)).join('\n')}\n`));
|
|
3917
4033
|
const file = `${dir}/session.v4.jsonl.zstd`;
|
|
3918
4034
|
try {
|
|
3919
4035
|
await writeFile(file, Buffer.concat([headerFrame, bodyFrame]));
|
|
3920
|
-
//
|
|
4036
|
+
// 回读校验:同一套容器契约(首帧恰好一行 header + seq 连续)
|
|
3921
4037
|
const back = decodeZstdLog(await readFile(file));
|
|
3922
4038
|
const defect = validateSessionLines(back.lines);
|
|
3923
4039
|
if (defect !== null) throw new Error(defect);
|
|
3924
4040
|
} catch (err) {
|
|
3925
4041
|
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
3926
|
-
throw new Error(
|
|
4042
|
+
throw new Error(`写出的会话未通过校验:${String(err && err.message ? err.message : err)}`);
|
|
3927
4043
|
}
|
|
3928
4044
|
return {
|
|
3929
4045
|
ok: true,
|
|
3930
4046
|
dryRun: false,
|
|
4047
|
+
source: bundle.source,
|
|
3931
4048
|
newId: bundle.id,
|
|
3932
4049
|
path: file,
|
|
3933
4050
|
messages: bundle.parsed.messages.length,
|
|
3934
4051
|
events: bundle.rows.length,
|
|
3935
|
-
summary: `💬 已导入
|
|
4052
|
+
summary: `💬 已导入 ${CHAT_SOURCE_LABELS[bundle.source]} 会话:${bundle.parsed.messages.length} 条消息(${bundle.rows.length} 个事件)\n新会话 id: ${bundle.id}\n重启 dsh 后可在会话列表看到(仅文本、推理摘要与工具调用/结果)`,
|
|
3936
4053
|
};
|
|
3937
4054
|
}
|
|
3938
|
-
|
|
3939
4055
|
/**
|
|
3940
4056
|
* 打开导出 ZIP(必要时用口令解密到备份目录临时文件)。返回 { zipPath, encrypted,
|
|
3941
4057
|
* cleanup };调用方在 finally 里必调 cleanup(解密出的明文 ZIP 不能遗留)。
|
|
@@ -5547,7 +5663,7 @@ export function apply(ctx, pluginConfig) {
|
|
|
5547
5663
|
const sub = parts[1] || 'list';
|
|
5548
5664
|
if (sub === 'list') {
|
|
5549
5665
|
const r = await chatSources();
|
|
5550
|
-
const lines = r.sessions.map((s) => ` ${s.id} ${s.messages} 条 ${s.title ? `「${s.title}」` : ''}`);
|
|
5666
|
+
const lines = r.sessions.map((s) => ` ${CHAT_SOURCE_LABELS[s.source] ?? s.source} ${s.id} ${s.messages} 条 ${s.title ? `「${s.title}」` : ''}`);
|
|
5551
5667
|
return { kind: 'success', text: `${r.summary}${lines.length ? `\n${lines.join('\n')}\n\n预览:/backup chat preview <id>;导入:/backup chat import <id> [--yes]` : ''}` };
|
|
5552
5668
|
}
|
|
5553
5669
|
if (sub === 'preview' || sub === 'import') {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xiaoyuyu6420/dsh-backup",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.1",
|
|
4
4
|
"description": "一条命令备份、恢复与迁移 DeepSeek Harness(dsh)的全部数据(~/.dsh:会话、设置、插件、MCP、技能):定时自动备份、升级前快照、换机迁移、会话日志体检修复、宿主起不来也能用的进程外救援;凭据默认脱敏只存本机。 Backup, restore & migrate all DeepSeek Harness (~/.dsh) data — sessions, settings, plugins, MCP, skills: scheduled backups, pre-upgrade snapshots, cross-machine migration, session-log doctor, out-of-process rescue console; credentials redacted to a local-only vault.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.24.0",
|