@waterwx/dsh-novel-forge 1.3.2 → 1.4.0
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 +2 -2
- package/lib/client.js +548 -267
- package/lib/client.js.map +1 -1
- package/lib/index.js +183 -9
- package/lib/index.js.map +1 -1
- package/lib/types/bookshelf.d.ts +6 -1
- package/lib/types/client/api.d.ts +4 -0
- package/lib/types/client/panel/ImportModal.d.ts +7 -0
- package/lib/types/client/panel/ShelfView.d.ts +3 -1
- package/lib/types/engine.d.ts +6 -0
- package/lib/types/index.d.ts +1 -1
- package/lib/types/protocol.d.ts +32 -0
- package/package.json +97 -97
- package/src/bookshelf.ts +18 -2
- package/src/client/api.ts +420 -410
- package/src/client/panel/ImportModal.tsx +191 -0
- package/src/client/panel/NovelPanel.tsx +5058 -5046
- package/src/client/panel/ShelfView.tsx +249 -236
- package/src/client/panel/panel.module.css +3037 -2941
- package/src/engine.ts +3001 -2943
- package/src/index.ts +7 -5
- package/src/protocol.ts +1244 -1207
- package/src/routes.ts +2332 -2272
- package/scripts/assistant-frames.txt +0 -8
- package/scripts/diagnose-adapter.mts +0 -81
- package/scripts/diagnose-assistant.mjs +0 -58
- package/scripts/diagnose-bible.mjs +0 -72
- package/scripts/diagnose-harness.mts +0 -75
- package/scripts/novel-forge-restart.log +0 -10
- package/scripts/novel-forge-web.stderr.log +0 -2
- package/scripts/novel-forge-web.stdout.log +0 -1
- package/scripts/outline-sample.txt +0 -449
- package/scripts/probe.mjs +0 -6
- package/scripts/restart-web.ps1 +0 -88
- package/scripts/smoke.mts +0 -72
- package/scripts/upload-github.ps1 +0 -51
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 导入小说弹窗:两种模式 ——
|
|
3
|
+
* A) 已有项目目录(含 novel-project.json)→ 登记/激活书架;
|
|
4
|
+
* B) txt/md 全本 → 服务器拆章建项目后登记书架。
|
|
5
|
+
* 浏览器无法直接浏览宿主机目录,故两种模式均输入绝对路径(粘贴或手输)。
|
|
6
|
+
*/
|
|
7
|
+
import { useState } from 'react'
|
|
8
|
+
import type { NovelApi } from '../api.ts'
|
|
9
|
+
import css from './panel.module.css'
|
|
10
|
+
|
|
11
|
+
type ImportMode = 'dir' | 'text'
|
|
12
|
+
|
|
13
|
+
/** 导入结果(成功态展示)。 */
|
|
14
|
+
interface ImportResult {
|
|
15
|
+
kind: 'dir' | 'text'
|
|
16
|
+
bookName: string
|
|
17
|
+
existed?: boolean
|
|
18
|
+
chapters?: number
|
|
19
|
+
skipped?: string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function ImportModal({
|
|
23
|
+
api,
|
|
24
|
+
onClose,
|
|
25
|
+
onImported,
|
|
26
|
+
}: {
|
|
27
|
+
api: NovelApi
|
|
28
|
+
onClose: () => void
|
|
29
|
+
/** 导入成功并已激活该书后回调(刷新书架)。 */
|
|
30
|
+
onImported: () => void | Promise<void>
|
|
31
|
+
}) {
|
|
32
|
+
const [mode, setMode] = useState<ImportMode>('dir')
|
|
33
|
+
const [dir, setDir] = useState('')
|
|
34
|
+
const [filePath, setFilePath] = useState('')
|
|
35
|
+
const [outDir, setOutDir] = useState('')
|
|
36
|
+
const [busy, setBusy] = useState(false)
|
|
37
|
+
const [error, setError] = useState('')
|
|
38
|
+
const [result, setResult] = useState<ImportResult | null>(null)
|
|
39
|
+
|
|
40
|
+
const runImport = async () => {
|
|
41
|
+
setError('')
|
|
42
|
+
setResult(null)
|
|
43
|
+
if (mode === 'dir') {
|
|
44
|
+
const d = dir.trim()
|
|
45
|
+
if (d === '') { setError('请输入项目目录(含 novel-project.json 的文件夹)'); return }
|
|
46
|
+
setBusy(true)
|
|
47
|
+
try {
|
|
48
|
+
const r = await api.bookImportDir(d)
|
|
49
|
+
setResult({ kind: 'dir', bookName: r.book.bookName, existed: r.existed })
|
|
50
|
+
await onImported()
|
|
51
|
+
} catch (err) {
|
|
52
|
+
setError((err as Error).message)
|
|
53
|
+
} finally {
|
|
54
|
+
setBusy(false)
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
const f = filePath.trim()
|
|
58
|
+
if (f === '') { setError('请输入 txt/md 文件路径'); return }
|
|
59
|
+
setBusy(true)
|
|
60
|
+
try {
|
|
61
|
+
const o = outDir.trim()
|
|
62
|
+
const r = await api.bookImportText(f, o !== '' ? o : undefined)
|
|
63
|
+
setResult({ kind: 'text', bookName: r.bookName, chapters: r.chapters, skipped: r.skipped })
|
|
64
|
+
await onImported()
|
|
65
|
+
} catch (err) {
|
|
66
|
+
setError((err as Error).message)
|
|
67
|
+
} finally {
|
|
68
|
+
setBusy(false)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={css.importModalOverlay} onClick={e => { if (e.target === e.currentTarget) onClose() }}>
|
|
75
|
+
<div className={css.importModal}>
|
|
76
|
+
<div className={css.importModalHead}>
|
|
77
|
+
<span className={css.panelTitle} style={{ margin: 0 }}>📥 导入小说</span>
|
|
78
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={onClose} title="关闭">✕</button>
|
|
79
|
+
</div>
|
|
80
|
+
|
|
81
|
+
<div className={css.importModalTabs} role="tablist">
|
|
82
|
+
<button
|
|
83
|
+
type="button"
|
|
84
|
+
role="tab"
|
|
85
|
+
aria-selected={mode === 'dir'}
|
|
86
|
+
className={`${css.button} ${mode === 'dir' ? css.buttonPrimary : ''}`}
|
|
87
|
+
onClick={() => { setMode('dir'); setError(''); setResult(null) }}
|
|
88
|
+
>
|
|
89
|
+
📂 A · 已有项目目录
|
|
90
|
+
</button>
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
role="tab"
|
|
94
|
+
aria-selected={mode === 'text'}
|
|
95
|
+
className={`${css.button} ${mode === 'text' ? css.buttonPrimary : ''}`}
|
|
96
|
+
onClick={() => { setMode('text'); setError(''); setResult(null) }}
|
|
97
|
+
>
|
|
98
|
+
📄 B · txt/md 全本
|
|
99
|
+
</button>
|
|
100
|
+
</div>
|
|
101
|
+
|
|
102
|
+
<div className={css.importModalBody}>
|
|
103
|
+
{mode === 'dir' ? (
|
|
104
|
+
<>
|
|
105
|
+
<label className={css.importField}>
|
|
106
|
+
<span>项目目录(绝对路径)</span>
|
|
107
|
+
<input
|
|
108
|
+
className={css.input}
|
|
109
|
+
type="text"
|
|
110
|
+
placeholder="例如 D:\novels\我的小说"
|
|
111
|
+
value={dir}
|
|
112
|
+
onChange={e => { setDir(e.target.value) }}
|
|
113
|
+
/>
|
|
114
|
+
</label>
|
|
115
|
+
<div className={css.importHint}>
|
|
116
|
+
目录需含 novel-project.json。已在书架中的目录会直接切换激活,不会重复登记。
|
|
117
|
+
</div>
|
|
118
|
+
</>
|
|
119
|
+
) : (
|
|
120
|
+
<>
|
|
121
|
+
<label className={css.importField}>
|
|
122
|
+
<span>全本文本文件(绝对路径,txt / md)</span>
|
|
123
|
+
<input
|
|
124
|
+
className={css.input}
|
|
125
|
+
type="text"
|
|
126
|
+
placeholder="例如 D:\novels\全本.txt"
|
|
127
|
+
value={filePath}
|
|
128
|
+
onChange={e => { setFilePath(e.target.value) }}
|
|
129
|
+
/>
|
|
130
|
+
</label>
|
|
131
|
+
<label className={css.importField}>
|
|
132
|
+
<span>输出目录(可选,默认 ~/.dsh/novels/书名)</span>
|
|
133
|
+
<input
|
|
134
|
+
className={css.input}
|
|
135
|
+
type="text"
|
|
136
|
+
placeholder="留空使用默认目录"
|
|
137
|
+
value={outDir}
|
|
138
|
+
onChange={e => { setOutDir(e.target.value) }}
|
|
139
|
+
/>
|
|
140
|
+
</label>
|
|
141
|
+
<div className={css.importHint}>
|
|
142
|
+
服务器按「第X章 / 第X回 / 第X节 / 第X卷」(可带 # 前缀)拆章;正文过短(<50 字)的章节会跳过并列出。
|
|
143
|
+
</div>
|
|
144
|
+
</>
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
{error !== '' && <div className={css.importError}>{error}</div>}
|
|
148
|
+
|
|
149
|
+
{result !== null && (
|
|
150
|
+
<div className={css.importResult}>
|
|
151
|
+
{result.kind === 'dir' ? (
|
|
152
|
+
<>
|
|
153
|
+
<span>✅ 已{result.existed === true ? '重新激活' : '登记'}《{result.bookName}》</span>
|
|
154
|
+
<span className={css.meta}>
|
|
155
|
+
{result.existed === true
|
|
156
|
+
? '该目录已在书架中,现已切换为当前书;工作台数据按该书目录加载。'
|
|
157
|
+
: '已加入书架并设为当前书,进入工作台即可看到全部模块。'
|
|
158
|
+
}
|
|
159
|
+
</span>
|
|
160
|
+
</>
|
|
161
|
+
) : (
|
|
162
|
+
<>
|
|
163
|
+
<span>✅ 《{result.bookName}》导入完成:{result.chapters} 章</span>
|
|
164
|
+
{(result.skipped ?? []).length > 0 && (
|
|
165
|
+
<span className={css.meta}>
|
|
166
|
+
跳过 {result.skipped!.length} 个过短章节:{result.skipped!.slice(0, 8).join('、')}
|
|
167
|
+
{result.skipped!.length > 8 ? ` 等 ${result.skipped!.length} 个` : ''}
|
|
168
|
+
</span>
|
|
169
|
+
)}
|
|
170
|
+
<span className={css.meta}>章节已按「written」登记,进入工作台后逐章审稿/补设定即可。</span>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
</div>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
|
|
177
|
+
<div className={css.importModalActions}>
|
|
178
|
+
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={onClose}>关闭</button>
|
|
179
|
+
<button
|
|
180
|
+
type="button"
|
|
181
|
+
className={`${css.button} ${css.buttonPrimary}`}
|
|
182
|
+
disabled={busy}
|
|
183
|
+
onClick={runImport}
|
|
184
|
+
>
|
|
185
|
+
{busy ? '导入中…' : '开始导入'}
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
)
|
|
191
|
+
}
|