iivo-sub 0.1.1 → 0.1.3
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 +6 -0
- package/bin/iivo-sub.js +315 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,11 +26,17 @@ The main menu includes:
|
|
|
26
26
|
- Quick configuration
|
|
27
27
|
- Backup configuration
|
|
28
28
|
- Restore backup
|
|
29
|
+
- Backup chat records
|
|
30
|
+
- Restore chat records
|
|
29
31
|
- Clear cache and backups
|
|
30
32
|
- Exit
|
|
31
33
|
|
|
32
34
|
Backup configuration lets you choose Codex, Claude Code, Hermes, OpenClaw, or all targets. You can name the backup yourself; the default name is `<date-time>_<target>`.
|
|
33
35
|
|
|
36
|
+
Chat record backup lets you back up common local conversation paths for Codex, Claude Code, Hermes, and OpenClaw. Restore creates a safety backup before replacing existing chat record paths.
|
|
37
|
+
|
|
38
|
+
For Codex, chat backup includes sessions, the recent-task index, history, attachments, shell snapshots, and local state files. Restoring a new backup keeps the original recent-task index; restoring an older Codex chat backup that only contains sessions will rebuild the recent-task index automatically.
|
|
39
|
+
|
|
34
40
|
After you enter an API key once, it is cached in `~/.iivo-sub/cache.json`. Next time the prompt shows a masked key such as `sk-x...abcd`; press Enter to reuse it or type a new key to replace it.
|
|
35
41
|
|
|
36
42
|
Existing config files are backed up under `~/.iivo-sub/backups/` before being replaced.
|
package/bin/iivo-sub.js
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os'
|
|
|
5
5
|
import path from 'node:path'
|
|
6
6
|
import readline from 'node:readline'
|
|
7
7
|
|
|
8
|
-
const VERSION = '0.1.
|
|
8
|
+
const VERSION = '0.1.3'
|
|
9
9
|
const APP_DIR = path.join(os.homedir(), '.iivo-sub')
|
|
10
10
|
const CACHE_FILE = path.join(APP_DIR, 'cache.json')
|
|
11
11
|
const SCRIPTED_INPUT = !process.stdin.isTTY ? fs.readFileSync(0, 'utf8').split(/\r?\n/) : null
|
|
@@ -40,6 +40,8 @@ const MAIN_MENU = [
|
|
|
40
40
|
{ label: '快速配置', value: 'quick' },
|
|
41
41
|
{ label: '备份配置', value: 'backup' },
|
|
42
42
|
{ label: '恢复备份', value: 'restore' },
|
|
43
|
+
{ label: '备份聊天记录', value: 'backup-chats' },
|
|
44
|
+
{ label: '还原聊天记录', value: 'restore-chats' },
|
|
43
45
|
{ label: '清空缓存及备份', value: 'clear-cache' },
|
|
44
46
|
{ label: '退出', value: 'exit' }
|
|
45
47
|
]
|
|
@@ -52,6 +54,14 @@ const BACKUP_TARGETS = [
|
|
|
52
54
|
{ label: '全部备份', value: 'all' }
|
|
53
55
|
]
|
|
54
56
|
|
|
57
|
+
const CHAT_BACKUP_TARGETS = [
|
|
58
|
+
{ label: 'Codex 聊天记录', value: 'codex' },
|
|
59
|
+
{ label: 'Claude Code 聊天记录', value: 'claude-code' },
|
|
60
|
+
{ label: 'Hermes 聊天记录', value: 'hermes' },
|
|
61
|
+
{ label: 'OpenClaw 聊天记录', value: 'openclaw' },
|
|
62
|
+
{ label: '全部聊天记录', value: 'all' }
|
|
63
|
+
]
|
|
64
|
+
|
|
55
65
|
const RESTORE_CONFIRM_OPTIONS = [
|
|
56
66
|
{ label: '确认恢复', value: 'yes' },
|
|
57
67
|
{ label: '取消', value: 'no' }
|
|
@@ -260,6 +270,10 @@ function fileExists(filePath) {
|
|
|
260
270
|
return fs.existsSync(filePath) && fs.statSync(filePath).isFile()
|
|
261
271
|
}
|
|
262
272
|
|
|
273
|
+
function pathExists(targetPath) {
|
|
274
|
+
return fs.existsSync(targetPath)
|
|
275
|
+
}
|
|
276
|
+
|
|
263
277
|
function configTargets() {
|
|
264
278
|
return {
|
|
265
279
|
codex: [
|
|
@@ -301,6 +315,50 @@ function filesForBackupTarget(target) {
|
|
|
301
315
|
return targets[target] ?? []
|
|
302
316
|
}
|
|
303
317
|
|
|
318
|
+
function chatTargets() {
|
|
319
|
+
return {
|
|
320
|
+
codex: [
|
|
321
|
+
path.join(os.homedir(), '.codex', 'sessions'),
|
|
322
|
+
path.join(os.homedir(), '.codex', 'session_index.jsonl'),
|
|
323
|
+
path.join(os.homedir(), '.codex', 'history.jsonl'),
|
|
324
|
+
path.join(os.homedir(), '.codex', 'attachments'),
|
|
325
|
+
path.join(os.homedir(), '.codex', 'shell_snapshots'),
|
|
326
|
+
...matchingHomeFiles('.codex', /^(?:state|goals|logs|memories)_\d+\.sqlite(?:-(?:shm|wal))?$/)
|
|
327
|
+
],
|
|
328
|
+
'claude-code': [
|
|
329
|
+
path.join(os.homedir(), '.claude', 'projects'),
|
|
330
|
+
path.join(os.homedir(), '.claude', 'todos'),
|
|
331
|
+
path.join(os.homedir(), '.claude', 'shell-snapshots')
|
|
332
|
+
],
|
|
333
|
+
hermes: [
|
|
334
|
+
path.join(os.homedir(), '.hermes', 'sessions'),
|
|
335
|
+
path.join(os.homedir(), '.hermes', 'chats'),
|
|
336
|
+
path.join(os.homedir(), '.hermes', 'history.json')
|
|
337
|
+
],
|
|
338
|
+
openclaw: [
|
|
339
|
+
path.join(os.homedir(), '.openclaw', 'sessions'),
|
|
340
|
+
path.join(os.homedir(), '.openclaw', 'chats'),
|
|
341
|
+
path.join(os.homedir(), '.openclaw', 'history.json')
|
|
342
|
+
]
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function matchingHomeFiles(relativeDir, pattern) {
|
|
347
|
+
const dir = path.join(os.homedir(), relativeDir)
|
|
348
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return []
|
|
349
|
+
return fs.readdirSync(dir)
|
|
350
|
+
.filter((name) => pattern.test(name))
|
|
351
|
+
.map((name) => path.join(dir, name))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function pathsForChatBackupTarget(target) {
|
|
355
|
+
const targets = chatTargets()
|
|
356
|
+
if (target === 'all') {
|
|
357
|
+
return Object.values(targets).flat()
|
|
358
|
+
}
|
|
359
|
+
return targets[target] ?? []
|
|
360
|
+
}
|
|
361
|
+
|
|
304
362
|
function uniqueFiles(files) {
|
|
305
363
|
return [...new Set(files)]
|
|
306
364
|
}
|
|
@@ -309,6 +367,10 @@ function backupTargetLabel(value) {
|
|
|
309
367
|
return BACKUP_TARGETS.find((item) => item.value === value)?.label ?? value
|
|
310
368
|
}
|
|
311
369
|
|
|
370
|
+
function chatBackupTargetLabel(value) {
|
|
371
|
+
return CHAT_BACKUP_TARGETS.find((item) => item.value === value)?.label ?? value
|
|
372
|
+
}
|
|
373
|
+
|
|
312
374
|
function timestampForName() {
|
|
313
375
|
const now = new Date()
|
|
314
376
|
const pad = (value) => String(value).padStart(2, '0')
|
|
@@ -349,6 +411,134 @@ function copyConfigToBackup(filePath, backupRoot) {
|
|
|
349
411
|
}
|
|
350
412
|
}
|
|
351
413
|
|
|
414
|
+
function copyPathToBackup(sourcePath, backupRoot) {
|
|
415
|
+
const stat = fs.statSync(sourcePath)
|
|
416
|
+
const backupFile = toBackupFileName(sourcePath)
|
|
417
|
+
const backupPath = path.join(backupRoot, 'files', backupFile)
|
|
418
|
+
ensureDir(path.dirname(backupPath))
|
|
419
|
+
fs.cpSync(sourcePath, backupPath, { recursive: true, force: true })
|
|
420
|
+
return {
|
|
421
|
+
source: sourcePath,
|
|
422
|
+
backup: path.relative(backupRoot, backupPath),
|
|
423
|
+
type: stat.isDirectory() ? 'directory' : 'file'
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function codexSessionIDFromPath(filePath) {
|
|
428
|
+
const base = path.basename(filePath, '.jsonl')
|
|
429
|
+
const match = base.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})$/i)
|
|
430
|
+
return match ? match[1] : ''
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function truncateTitle(text) {
|
|
434
|
+
let value = String(text || '')
|
|
435
|
+
const requestMatch = value.match(/(?:##\s*)?My request for Codex:\s*([\s\S]+)/i)
|
|
436
|
+
if (requestMatch) {
|
|
437
|
+
value = requestMatch[1]
|
|
438
|
+
}
|
|
439
|
+
value = value
|
|
440
|
+
.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, ' ')
|
|
441
|
+
.replace(/# Context from my IDE setup:/gi, ' ')
|
|
442
|
+
.replace(/## Open tabs:[\s\S]*?(?=##|$)/gi, ' ')
|
|
443
|
+
.replace(/## Active file:[\s\S]*?(?=##|$)/gi, ' ')
|
|
444
|
+
return value
|
|
445
|
+
.replace(/\s+/g, ' ')
|
|
446
|
+
.trim()
|
|
447
|
+
.slice(0, 80)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function titleFromCodexSession(filePath) {
|
|
451
|
+
try {
|
|
452
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
453
|
+
for (const line of content.split(/\r?\n/)) {
|
|
454
|
+
if (!line.trim()) continue
|
|
455
|
+
let item
|
|
456
|
+
try {
|
|
457
|
+
item = JSON.parse(line)
|
|
458
|
+
} catch {
|
|
459
|
+
continue
|
|
460
|
+
}
|
|
461
|
+
const payload = item?.payload
|
|
462
|
+
if (payload?.type === 'user_message' && payload.message) {
|
|
463
|
+
return truncateTitle(payload.message)
|
|
464
|
+
}
|
|
465
|
+
if (payload?.type === 'message' && payload.role === 'user' && Array.isArray(payload.content)) {
|
|
466
|
+
const text = payload.content
|
|
467
|
+
.map((part) => part?.text || part?.input_text || '')
|
|
468
|
+
.filter(Boolean)
|
|
469
|
+
.join(' ')
|
|
470
|
+
if (text) return truncateTitle(text)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} catch {
|
|
474
|
+
return ''
|
|
475
|
+
}
|
|
476
|
+
return ''
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function latestTimestampFromCodexSession(filePath) {
|
|
480
|
+
let latest = ''
|
|
481
|
+
try {
|
|
482
|
+
const content = fs.readFileSync(filePath, 'utf8')
|
|
483
|
+
for (const line of content.split(/\r?\n/)) {
|
|
484
|
+
if (!line.trim()) continue
|
|
485
|
+
try {
|
|
486
|
+
const item = JSON.parse(line)
|
|
487
|
+
if (typeof item.timestamp === 'string' && item.timestamp > latest) {
|
|
488
|
+
latest = item.timestamp
|
|
489
|
+
}
|
|
490
|
+
} catch {
|
|
491
|
+
// Ignore malformed lines.
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch {
|
|
495
|
+
return ''
|
|
496
|
+
}
|
|
497
|
+
return latest
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function listCodexSessionFiles(dir) {
|
|
501
|
+
if (!fs.existsSync(dir)) return []
|
|
502
|
+
const out = []
|
|
503
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
504
|
+
const fullPath = path.join(dir, entry.name)
|
|
505
|
+
if (entry.isDirectory()) {
|
|
506
|
+
out.push(...listCodexSessionFiles(fullPath))
|
|
507
|
+
} else if (entry.isFile() && entry.name.endsWith('.jsonl')) {
|
|
508
|
+
out.push(fullPath)
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
return out
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function rebuildCodexSessionIndex() {
|
|
515
|
+
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions')
|
|
516
|
+
const indexPath = path.join(os.homedir(), '.codex', 'session_index.jsonl')
|
|
517
|
+
const files = listCodexSessionFiles(sessionsDir)
|
|
518
|
+
if (files.length === 0) return 0
|
|
519
|
+
|
|
520
|
+
const rows = files.map((filePath) => {
|
|
521
|
+
const id = codexSessionIDFromPath(filePath)
|
|
522
|
+
if (!id) return null
|
|
523
|
+
const title = titleFromCodexSession(filePath) || id
|
|
524
|
+
const stat = fs.statSync(filePath)
|
|
525
|
+
return {
|
|
526
|
+
id,
|
|
527
|
+
thread_name: title,
|
|
528
|
+
updated_at: latestTimestampFromCodexSession(filePath) || stat.mtime.toISOString()
|
|
529
|
+
}
|
|
530
|
+
}).filter(Boolean)
|
|
531
|
+
.sort((a, b) => b.updated_at.localeCompare(a.updated_at))
|
|
532
|
+
|
|
533
|
+
if (rows.length === 0) return 0
|
|
534
|
+
ensureDir(path.dirname(indexPath))
|
|
535
|
+
if (fileExists(indexPath)) {
|
|
536
|
+
fs.copyFileSync(indexPath, `${indexPath}.${timestampForName()}.bak`)
|
|
537
|
+
}
|
|
538
|
+
fs.writeFileSync(indexPath, rows.map((row) => JSON.stringify(row)).join('\n') + '\n', 'utf8')
|
|
539
|
+
return rows.length
|
|
540
|
+
}
|
|
541
|
+
|
|
352
542
|
function loadBackupManifest(dir) {
|
|
353
543
|
const manifestPath = path.join(dir, 'manifest.json')
|
|
354
544
|
if (!fileExists(manifestPath)) return null
|
|
@@ -746,6 +936,7 @@ async function backupConfig() {
|
|
|
746
936
|
ensureDir(backupRoot)
|
|
747
937
|
const files = existing.map((filePath) => copyConfigToBackup(filePath, backupRoot))
|
|
748
938
|
const manifest = {
|
|
939
|
+
kind: 'config',
|
|
749
940
|
name,
|
|
750
941
|
target,
|
|
751
942
|
target_label: backupTargetLabel(target),
|
|
@@ -769,27 +960,105 @@ async function backupConfig() {
|
|
|
769
960
|
await pause()
|
|
770
961
|
}
|
|
771
962
|
|
|
963
|
+
async function backupChatRecords() {
|
|
964
|
+
clear()
|
|
965
|
+
banner()
|
|
966
|
+
const target = await select('请选择聊天记录备份目标:', CHAT_BACKUP_TARGETS)
|
|
967
|
+
if (target === '__exit__') return '__exit__'
|
|
968
|
+
if (!target) return
|
|
969
|
+
|
|
970
|
+
const defaultName = `${timestampForName()}_chats_${target}`
|
|
971
|
+
clear()
|
|
972
|
+
banner()
|
|
973
|
+
const rawName = await question('请输入聊天记录备份名称 [Esc 返回上一步]', { defaultValue: defaultName })
|
|
974
|
+
if (rawName === '__exit__') return '__exit__'
|
|
975
|
+
if (rawName === null) return backupChatRecords()
|
|
976
|
+
|
|
977
|
+
const name = safeBackupName(rawName) || defaultName
|
|
978
|
+
const backupRoot = path.join(APP_DIR, 'backups', name)
|
|
979
|
+
if (fs.existsSync(backupRoot)) {
|
|
980
|
+
console.log(c('red', `备份名称已存在: ${name}`))
|
|
981
|
+
await pause()
|
|
982
|
+
return backupChatRecords()
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
const candidates = uniqueFiles(pathsForChatBackupTarget(target))
|
|
986
|
+
const existing = candidates.filter(pathExists)
|
|
987
|
+
if (existing.length === 0) {
|
|
988
|
+
console.log(c('yellow', `没有找到可备份的 ${chatBackupTargetLabel(target)}。`))
|
|
989
|
+
console.log()
|
|
990
|
+
console.log(c('dim', '已检查常见目录:'))
|
|
991
|
+
for (const item of candidates) {
|
|
992
|
+
console.log(c('dim', `- ${item}`))
|
|
993
|
+
}
|
|
994
|
+
await pause()
|
|
995
|
+
return
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
ensureDir(backupRoot)
|
|
999
|
+
const files = existing.map((sourcePath) => copyPathToBackup(sourcePath, backupRoot))
|
|
1000
|
+
const manifest = {
|
|
1001
|
+
kind: 'chat',
|
|
1002
|
+
name,
|
|
1003
|
+
target,
|
|
1004
|
+
target_label: chatBackupTargetLabel(target),
|
|
1005
|
+
created_at: new Date().toISOString(),
|
|
1006
|
+
files
|
|
1007
|
+
}
|
|
1008
|
+
fs.writeFileSync(path.join(backupRoot, 'manifest.json'), json(manifest), 'utf8')
|
|
1009
|
+
|
|
1010
|
+
clear()
|
|
1011
|
+
banner()
|
|
1012
|
+
console.log(c('green', '聊天记录备份完成。'))
|
|
1013
|
+
console.log()
|
|
1014
|
+
console.log(`备份名称: ${name}`)
|
|
1015
|
+
console.log(`备份目标: ${chatBackupTargetLabel(target)}`)
|
|
1016
|
+
console.log(`备份目录: ${backupRoot}`)
|
|
1017
|
+
console.log()
|
|
1018
|
+
console.log(c('cyan', '已备份路径:'))
|
|
1019
|
+
for (const file of files) {
|
|
1020
|
+
console.log(`- ${file.source}`)
|
|
1021
|
+
}
|
|
1022
|
+
await pause()
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function backupEntryMatchesKind(entryName, backupDir, kind) {
|
|
1026
|
+
const manifest = loadBackupManifest(path.join(backupDir, entryName))
|
|
1027
|
+
if (!manifest) return kind === 'config'
|
|
1028
|
+
if (kind === 'config') return !manifest.kind || manifest.kind === 'config'
|
|
1029
|
+
return manifest.kind === kind
|
|
1030
|
+
}
|
|
1031
|
+
|
|
772
1032
|
async function restoreBackup() {
|
|
1033
|
+
return restoreBackupByKind('config', '请选择要查看的配置备份:', '暂无配置备份。')
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
async function restoreChatRecords() {
|
|
1037
|
+
return restoreBackupByKind('chat', '请选择要还原的聊天记录备份:', '暂无聊天记录备份。')
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
async function restoreBackupByKind(kind, title, emptyMessage) {
|
|
773
1041
|
clear()
|
|
774
1042
|
banner()
|
|
775
1043
|
const backupDir = path.join(APP_DIR, 'backups')
|
|
776
1044
|
if (!fs.existsSync(backupDir)) {
|
|
777
|
-
console.log(c('yellow',
|
|
1045
|
+
console.log(c('yellow', emptyMessage))
|
|
778
1046
|
return pause()
|
|
779
1047
|
}
|
|
780
1048
|
|
|
781
1049
|
const entries = fs.readdirSync(backupDir, { withFileTypes: true })
|
|
782
1050
|
.filter((entry) => entry.isDirectory())
|
|
783
1051
|
.map((entry) => entry.name)
|
|
1052
|
+
.filter((name) => backupEntryMatchesKind(name, backupDir, kind))
|
|
784
1053
|
.sort()
|
|
785
1054
|
.reverse()
|
|
786
1055
|
|
|
787
1056
|
if (entries.length === 0) {
|
|
788
|
-
console.log(c('yellow',
|
|
1057
|
+
console.log(c('yellow', emptyMessage))
|
|
789
1058
|
return pause()
|
|
790
1059
|
}
|
|
791
1060
|
|
|
792
|
-
const selected = await select(
|
|
1061
|
+
const selected = await select(title, [
|
|
793
1062
|
...entries.map((name) => ({ label: name, value: name })),
|
|
794
1063
|
{ label: '返回', value: null }
|
|
795
1064
|
])
|
|
@@ -816,38 +1085,64 @@ async function restoreBackup() {
|
|
|
816
1085
|
console.log(`备份目标: ${manifest.target_label || manifest.target || '未知'}`)
|
|
817
1086
|
console.log(`创建时间: ${manifest.created_at || '未知'}`)
|
|
818
1087
|
console.log()
|
|
819
|
-
console.log(c('cyan', '将恢复以下文件:'))
|
|
1088
|
+
console.log(c('cyan', kind === 'chat' ? '将还原以下聊天记录路径:' : '将恢复以下文件:'))
|
|
820
1089
|
for (const file of manifest.files || []) {
|
|
821
1090
|
console.log(`- ${file.source}`)
|
|
822
1091
|
}
|
|
823
1092
|
console.log()
|
|
824
1093
|
|
|
825
|
-
const
|
|
1094
|
+
const confirmText = kind === 'chat'
|
|
1095
|
+
? '确认还原这个聊天记录备份吗?当前同名聊天记录会先自动备份。'
|
|
1096
|
+
: '确认恢复这个备份吗?当前同名配置会先自动备份。'
|
|
1097
|
+
const confirm = await select(confirmText, RESTORE_CONFIRM_OPTIONS)
|
|
826
1098
|
if (confirm === '__exit__') return '__exit__'
|
|
827
1099
|
if (confirm !== 'yes') return
|
|
828
1100
|
|
|
829
1101
|
const safetyBackup = path.join(APP_DIR, 'backups', `${timestampForName()}_pre-restore_${safeBackupName(manifest.name || selected)}`)
|
|
830
1102
|
ensureDir(safetyBackup)
|
|
831
1103
|
const restored = []
|
|
1104
|
+
let restoredCodexSessions = false
|
|
1105
|
+
let restoredCodexIndex = false
|
|
1106
|
+
const codexSessionsPath = path.join(os.homedir(), '.codex', 'sessions')
|
|
1107
|
+
const codexIndexPath = path.join(os.homedir(), '.codex', 'session_index.jsonl')
|
|
832
1108
|
for (const file of manifest.files || []) {
|
|
833
1109
|
const source = file.source
|
|
834
1110
|
const backupPath = path.join(selectedDir, file.backup)
|
|
835
|
-
if (!source || !
|
|
836
|
-
if (
|
|
837
|
-
|
|
1111
|
+
if (!source || !pathExists(backupPath)) continue
|
|
1112
|
+
if (pathExists(source)) {
|
|
1113
|
+
copyPathToBackup(source, safetyBackup)
|
|
838
1114
|
}
|
|
839
1115
|
ensureDir(path.dirname(source))
|
|
840
|
-
|
|
1116
|
+
if (pathExists(source)) {
|
|
1117
|
+
deletePath(source)
|
|
1118
|
+
}
|
|
1119
|
+
fs.cpSync(backupPath, source, { recursive: true, force: true })
|
|
841
1120
|
restored.push(source)
|
|
1121
|
+
if (kind === 'chat' && source === codexSessionsPath) {
|
|
1122
|
+
restoredCodexSessions = true
|
|
1123
|
+
}
|
|
1124
|
+
if (kind === 'chat' && source === codexIndexPath) {
|
|
1125
|
+
restoredCodexIndex = true
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
let rebuiltCodexIndexCount = 0
|
|
1130
|
+
if (restoredCodexSessions && !restoredCodexIndex) {
|
|
1131
|
+
rebuiltCodexIndexCount = rebuildCodexSessionIndex()
|
|
842
1132
|
}
|
|
843
1133
|
|
|
844
1134
|
clear()
|
|
845
1135
|
banner()
|
|
846
|
-
console.log(c('green', '恢复完成。'))
|
|
1136
|
+
console.log(c('green', kind === 'chat' ? '聊天记录还原完成。' : '恢复完成。'))
|
|
847
1137
|
console.log()
|
|
848
1138
|
for (const file of restored) {
|
|
849
1139
|
console.log(`- ${file}`)
|
|
850
1140
|
}
|
|
1141
|
+
if (rebuiltCodexIndexCount > 0) {
|
|
1142
|
+
console.log(c('cyan', `已重建 Codex 任务索引: ${rebuiltCodexIndexCount} 条`))
|
|
1143
|
+
} else if (restoredCodexIndex) {
|
|
1144
|
+
console.log(c('cyan', '已恢复 Codex 任务索引。'))
|
|
1145
|
+
}
|
|
851
1146
|
console.log()
|
|
852
1147
|
console.log(c('yellow', `恢复前安全备份目录: ${safetyBackup}`))
|
|
853
1148
|
await pause()
|
|
@@ -858,7 +1153,7 @@ async function clearCacheAndBackups() {
|
|
|
858
1153
|
banner()
|
|
859
1154
|
console.log(c('yellow', '将删除以下 iivo-sub 数据:'))
|
|
860
1155
|
console.log(`- API Key 缓存: ${CACHE_FILE}`)
|
|
861
|
-
console.log(`- 备份目录: ${path.join(APP_DIR, 'backups')}`)
|
|
1156
|
+
console.log(`- 备份目录: ${path.join(APP_DIR, 'backups')} ${c('dim', '(包含配置备份和聊天记录备份)')}`)
|
|
862
1157
|
console.log(`- 临时配置脚本: ${path.join(APP_DIR, '*.env / *.ps1 / *.cmd')}`)
|
|
863
1158
|
console.log(`- 最近配置摘要: ${path.join(APP_DIR, 'config.json')}`)
|
|
864
1159
|
console.log()
|
|
@@ -925,6 +1220,14 @@ async function main() {
|
|
|
925
1220
|
const result = await restoreBackup()
|
|
926
1221
|
if (result === '__exit__') break
|
|
927
1222
|
}
|
|
1223
|
+
if (action === 'backup-chats') {
|
|
1224
|
+
const result = await backupChatRecords()
|
|
1225
|
+
if (result === '__exit__') break
|
|
1226
|
+
}
|
|
1227
|
+
if (action === 'restore-chats') {
|
|
1228
|
+
const result = await restoreChatRecords()
|
|
1229
|
+
if (result === '__exit__') break
|
|
1230
|
+
}
|
|
928
1231
|
if (action === 'clear-cache') {
|
|
929
1232
|
const result = await clearCacheAndBackups()
|
|
930
1233
|
if (result === '__exit__') break
|