@waterwx/dsh-novel-forge 1.3.1 → 1.3.2-alpha

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.
Files changed (161) hide show
  1. package/README.md +288 -279
  2. package/lib/client.js +56309 -12221
  3. package/lib/index.js +14239 -5595
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +36 -0
  6. package/lib/types/assets.d.ts +24 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +25 -0
  9. package/lib/types/bookshelf.d.ts +11 -1
  10. package/lib/types/client/api.d.ts +186 -10
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -0
  13. package/lib/types/client/index.d.ts +1 -1
  14. package/lib/types/client/llmLive.d.ts +31 -0
  15. package/lib/types/client/locales.d.ts +28 -67
  16. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  17. package/lib/types/client/panel/AiTag.d.ts +1 -0
  18. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  19. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  20. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  21. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  22. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  23. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  24. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  25. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  26. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  27. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  28. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  29. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  30. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  31. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  32. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  33. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  34. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  35. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  36. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  37. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  38. package/lib/types/client/panel/SettingsView.d.ts +19 -0
  39. package/lib/types/client/panel/ShelfView.d.ts +12 -1
  40. package/lib/types/client/panel/SubPage.d.ts +27 -0
  41. package/lib/types/client/panel/TensionView.d.ts +18 -0
  42. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  43. package/lib/types/client/panel/helpers.d.ts +5 -3
  44. package/lib/types/client/panel/toast.d.ts +2 -0
  45. package/lib/types/client/panel/views.d.ts +25 -7
  46. package/lib/types/client/run-status.d.ts +21 -0
  47. package/lib/types/client/text.d.ts +9 -0
  48. package/lib/types/docx-core.d.ts +16 -0
  49. package/lib/types/docx.d.ts +2 -3
  50. package/lib/types/engine.d.ts +186 -45
  51. package/lib/types/global-assets.d.ts +6 -0
  52. package/lib/types/index.d.ts +22 -8
  53. package/lib/types/llm-live.d.ts +75 -0
  54. package/lib/types/llm-retry.d.ts +38 -0
  55. package/lib/types/market-radar-scan.d.ts +19 -0
  56. package/lib/types/market-radar-sources.d.ts +31 -0
  57. package/lib/types/migrate-output.d.ts +25 -0
  58. package/lib/types/novel-context.d.ts +80 -0
  59. package/lib/types/prompt-slots.d.ts +57 -0
  60. package/lib/types/prompt-utils.d.ts +35 -0
  61. package/lib/types/prompting.d.ts +10 -0
  62. package/lib/types/protocol.d.ts +1362 -166
  63. package/lib/types/review-policy.d.ts +97 -0
  64. package/lib/types/revision.d.ts +76 -0
  65. package/lib/types/routes.d.ts +4 -0
  66. package/lib/types/run.d.ts +23 -1
  67. package/lib/types/snapshots.d.ts +46 -0
  68. package/lib/types/stage-contract.d.ts +60 -0
  69. package/lib/types/tension.d.ts +53 -0
  70. package/lib/types/timeline.d.ts +37 -0
  71. package/package.json +52 -28
  72. package/src/action-guard.ts +127 -0
  73. package/src/ai-scan.ts +179 -0
  74. package/src/assets.ts +1806 -520
  75. package/src/assistant.ts +1098 -770
  76. package/src/author-assets.ts +149 -0
  77. package/src/bookshelf.ts +187 -151
  78. package/src/client/api.ts +378 -21
  79. package/src/client/chip-prefs.ts +121 -0
  80. package/src/client/docx.ts +37 -69
  81. package/src/client/index.ts +3 -1
  82. package/src/client/llmLive.ts +111 -0
  83. package/src/client/locales.ts +28 -253
  84. package/src/client/mount.tsx +1 -1
  85. package/src/client/panel/AdaptModeView.tsx +535 -0
  86. package/src/client/panel/AiTag.tsx +7 -0
  87. package/src/client/panel/AssetsTab.tsx +448 -458
  88. package/src/client/panel/AssistantTab.tsx +355 -301
  89. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  90. package/src/client/panel/AuthorHome.tsx +309 -0
  91. package/src/client/panel/BookAnalysisView.tsx +63 -0
  92. package/src/client/panel/CmdPalette.tsx +102 -0
  93. package/src/client/panel/CreateBookView.tsx +366 -348
  94. package/src/client/panel/DirectorView.tsx +200 -0
  95. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  96. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  97. package/src/client/panel/ImportModal.tsx +303 -0
  98. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  99. package/src/client/panel/LiveFeedLog.tsx +204 -0
  100. package/src/client/panel/MarketRadarView.tsx +377 -0
  101. package/src/client/panel/ModelManager.tsx +205 -0
  102. package/src/client/panel/NovelPanel.tsx +3135 -2651
  103. package/src/client/panel/PageHeader.tsx +37 -0
  104. package/src/client/panel/ProgressConsole.tsx +63 -0
  105. package/src/client/panel/PromptSlotsView.tsx +97 -0
  106. package/src/client/panel/ReaderView.tsx +306 -306
  107. package/src/client/panel/ReasoningSection.tsx +31 -0
  108. package/src/client/panel/RunPanel.tsx +64 -32
  109. package/src/client/panel/SettingsView.tsx +354 -0
  110. package/src/client/panel/ShelfView.tsx +266 -236
  111. package/src/client/panel/SubPage.tsx +42 -0
  112. package/src/client/panel/TensionView.tsx +212 -0
  113. package/src/client/panel/TimelineView.tsx +208 -0
  114. package/src/client/panel/WorldTab.tsx +202 -202
  115. package/src/client/panel/helpers.ts +7 -5
  116. package/src/client/panel/panel.module.css +4481 -1655
  117. package/src/client/panel/toast.ts +51 -0
  118. package/src/client/panel/views.tsx +401 -314
  119. package/src/client/run-status.ts +171 -0
  120. package/src/client/sidebar-entry.ts +1 -1
  121. package/src/client/text.ts +28 -0
  122. package/src/docx-core.ts +53 -0
  123. package/src/docx.ts +44 -83
  124. package/src/engine.ts +4672 -2943
  125. package/src/global-assets.ts +59 -0
  126. package/src/index.ts +89 -33
  127. package/src/llm-live.ts +260 -0
  128. package/src/llm-retry.ts +77 -0
  129. package/src/market-radar-scan.ts +41 -0
  130. package/src/market-radar-sources.ts +173 -0
  131. package/src/migrate-output.ts +76 -0
  132. package/src/novel-context.ts +286 -0
  133. package/src/prompt-slots.ts +125 -0
  134. package/src/prompting.ts +26 -0
  135. package/src/protocol.ts +1388 -158
  136. package/src/review-policy.ts +161 -0
  137. package/src/revision.ts +283 -0
  138. package/src/routes.ts +1934 -434
  139. package/src/run.ts +118 -21
  140. package/src/snapshots.ts +174 -0
  141. package/src/stage-contract.ts +189 -0
  142. package/src/tension.ts +251 -0
  143. package/src/timeline.ts +159 -0
  144. package/lib/client.js.map +0 -1
  145. package/lib/index.js.map +0 -1
  146. package/lib/types/comic-presets.d.ts +0 -12
  147. package/scripts/assistant-frames.txt +0 -8
  148. package/scripts/diagnose-adapter.mts +0 -81
  149. package/scripts/diagnose-assistant.mjs +0 -58
  150. package/scripts/diagnose-bible.mjs +0 -72
  151. package/scripts/diagnose-harness.mts +0 -75
  152. package/scripts/novel-forge-restart.log +0 -10
  153. package/scripts/novel-forge-web.stderr.log +0 -2
  154. package/scripts/novel-forge-web.stdout.log +0 -1
  155. package/scripts/outline-sample.txt +0 -449
  156. package/scripts/probe.mjs +0 -6
  157. package/scripts/restart-web.ps1 +0 -88
  158. package/scripts/smoke.mts +0 -72
  159. package/scripts/upload-github.ps1 +0 -51
  160. package/src/client/panel/BookshelfBar.tsx +0 -170
  161. package/src/comic-presets.ts +0 -67
@@ -1,202 +1,202 @@
1
- /**
2
- * 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 + AI 提炼。
3
- * 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
4
- */
5
- import { useEffect, useState } from 'react'
6
- import type { NovelApi } from '../api.ts'
7
- import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
8
- import css from './panel.module.css'
9
-
10
- const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
11
-
12
- /** 一个可编辑条目行。 */
13
- function EditableRow({
14
- name,
15
- detail,
16
- onName,
17
- onDetail,
18
- onRemove,
19
- namePlaceholder,
20
- detailPlaceholder,
21
- }: {
22
- name: string
23
- detail: string
24
- onName: (v: string) => void
25
- onDetail: (v: string) => void
26
- onRemove: () => void
27
- namePlaceholder: string
28
- detailPlaceholder: string
29
- }) {
30
- return (
31
- <div style={{ display: 'flex', gap: 6, alignItems: 'flex-start' }}>
32
- <input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
33
- <input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
34
- <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
35
- </div>
36
- )
37
- }
38
-
39
- /** 大世界页签。 */
40
- export function WorldTab({
41
- api,
42
- world,
43
- onChanged,
44
- }: {
45
- api: NovelApi
46
- /** 当前项目的大世界数据(可能为空)。 */
47
- world: WorldState | undefined
48
- /** 保存/提炼成功后由父组件刷新项目状态。 */
49
- onChanged: (world: WorldState) => void
50
- }) {
51
- const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
52
- const [busy, setBusy] = useState(false)
53
- const [error, setError] = useState('')
54
-
55
- // 外部数据变化时同步草稿(提炼/保存/切换书后)。
56
- useEffect(() => {
57
- setDraft(world ?? EMPTY)
58
- }, [world])
59
-
60
- const handleGenerate = async (): Promise<void> => {
61
- setBusy(true)
62
- setError('')
63
- try {
64
- const result = await api.world('generate')
65
- setDraft(result.world)
66
- onChanged(result.world)
67
- } catch (err) {
68
- setError((err as Error).message)
69
- } finally {
70
- setBusy(false)
71
- }
72
- }
73
-
74
- const handleSave = async (): Promise<void> => {
75
- setBusy(true)
76
- setError('')
77
- try {
78
- const result = await api.world('save', draft)
79
- onChanged(result.world)
80
- } catch (err) {
81
- setError((err as Error).message)
82
- } finally {
83
- setBusy(false)
84
- }
85
- }
86
-
87
- const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
88
- const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
89
- const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
90
-
91
- return (
92
- <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
93
- {error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
94
-
95
- <div className={css.card}>
96
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
97
- <span className={css.cardTitle}>大世界</span>
98
- <div className={css.row}>
99
- <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
100
- ✨ AI 提炼
101
- </button>
102
- <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
103
- 💾 保存
104
- </button>
105
- </div>
106
- </div>
107
- <span className={css.meta}>
108
- 境界体系按由低到高排序注入章节生成提示词,模型不得随意跳级或自创境界;区域与势力约束地理/势力设定。AI 提炼不满意可逐条编辑后保存。
109
- </span>
110
- </div>
111
-
112
- {/* 境界体系 */}
113
- <div className={css.card}>
114
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
115
- <span className={css.cardTitle}>境界体系({draft.realms.length})</span>
116
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
117
- + 新增境界
118
- </button>
119
- </div>
120
- {draft.realms.length === 0 ? (
121
- <span className={css.meta}>暂无境界体系 — 点击 ✨AI 提炼 或手动添加(由低到高)。</span>
122
- ) : (
123
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
124
- {draft.realms.map((realm, i) => (
125
- <EditableRow
126
- key={i}
127
- name={realm.name}
128
- detail={realm.description}
129
- onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
130
- onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
131
- onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
132
- namePlaceholder={`第 ${i + 1} 阶境界名`}
133
- detailPlaceholder="突破条件 / 寿命 / 标志…"
134
- />
135
- ))}
136
- </div>
137
- )}
138
- </div>
139
-
140
- {/* 地理区域 */}
141
- <div className={css.card}>
142
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
143
- <span className={css.cardTitle}>地理区域({draft.regions.length})</span>
144
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
145
- + 新增区域
146
- </button>
147
- </div>
148
- {draft.regions.length === 0 ? (
149
- <span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
150
- ) : (
151
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
152
- {draft.regions.map((region, i) => (
153
- <EditableRow
154
- key={i}
155
- name={region.name}
156
- detail={region.description}
157
- onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
158
- onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
159
- onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
160
- namePlaceholder="区域名(如大荒 / 青云山脉)"
161
- detailPlaceholder="描述…"
162
- />
163
- ))}
164
- </div>
165
- )}
166
- </div>
167
-
168
- {/* 势力分布 */}
169
- <div className={css.card}>
170
- <div className={css.row} style={{ justifyContent: 'space-between' }}>
171
- <span className={css.cardTitle}>势力分布({draft.factions.length})</span>
172
- <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
173
- + 新增势力
174
- </button>
175
- </div>
176
- {draft.factions.length === 0 ? (
177
- <span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
178
- ) : (
179
- <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
180
- {draft.factions.map((faction, i) => (
181
- <EditableRow
182
- key={i}
183
- name={`${faction.name}(${faction.kind})`}
184
- detail={faction.description}
185
- onName={v => {
186
- const next = [...draft.factions]
187
- const match = /^(.*)((.*))$/.exec(v)
188
- next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
189
- setFactions(next)
190
- }}
191
- onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
192
- onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
193
- namePlaceholder="势力名(类型)如:青云宗(宗门)"
194
- detailPlaceholder="描述…"
195
- />
196
- ))}
197
- </div>
198
- )}
199
- </div>
200
- </div>
201
- )
202
- }
1
+ /**
2
+ * 大世界页签:境界体系 / 地理区域 / 势力分布 的结构化编辑 + 提炼。
3
+ * 数据注入每章生成与审稿提示词(renderWorld),保证设定不写飞。
4
+ */
5
+ import { useEffect, useState } from 'react'
6
+ import type { NovelApi } from '../api.ts'
7
+ import type { WorldFaction, WorldRealm, WorldRegion, WorldState } from '../../protocol.ts'
8
+ import css from './panel.module.css'
9
+
10
+ const EMPTY: WorldState = { realms: [], regions: [], factions: [] }
11
+
12
+ /** 一个可编辑条目行。 */
13
+ function EditableRow({
14
+ name,
15
+ detail,
16
+ onName,
17
+ onDetail,
18
+ onRemove,
19
+ namePlaceholder,
20
+ detailPlaceholder,
21
+ }: {
22
+ name: string
23
+ detail: string
24
+ onName: (v: string) => void
25
+ onDetail: (v: string) => void
26
+ onRemove: () => void
27
+ namePlaceholder: string
28
+ detailPlaceholder: string
29
+ }) {
30
+ return (
31
+ <div style={{ display: 'flex', gap: 'var(--nf-space-6)', alignItems: 'flex-start' }}>
32
+ <input className={css.input} style={{ flex: 2, minWidth: 0 }} placeholder={namePlaceholder} value={name} onChange={e => { onName(e.target.value) }} />
33
+ <input className={css.input} style={{ flex: 3, minWidth: 0 }} placeholder={detailPlaceholder} value={detail} onChange={e => { onDetail(e.target.value) }} />
34
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} title="删除" onClick={onRemove}>×</button>
35
+ </div>
36
+ )
37
+ }
38
+
39
+ /** 大世界页签。 */
40
+ export function WorldTab({
41
+ api,
42
+ world,
43
+ onChanged,
44
+ }: {
45
+ api: NovelApi
46
+ /** 当前项目的大世界数据(可能为空)。 */
47
+ world: WorldState | undefined
48
+ /** 保存/提炼成功后由父组件刷新项目状态。 */
49
+ onChanged: (world: WorldState) => void
50
+ }) {
51
+ const [draft, setDraft] = useState<WorldState>(() => world ?? EMPTY)
52
+ const [busy, setBusy] = useState(false)
53
+ const [error, setError] = useState('')
54
+
55
+ // 外部数据变化时同步草稿(提炼/保存/切换书后)。
56
+ useEffect(() => {
57
+ setDraft(world ?? EMPTY)
58
+ }, [world])
59
+
60
+ const handleGenerate = async (): Promise<void> => {
61
+ setBusy(true)
62
+ setError('')
63
+ try {
64
+ const result = await api.world('generate')
65
+ setDraft(result.world)
66
+ onChanged(result.world)
67
+ } catch (err) {
68
+ setError((err as Error).message)
69
+ } finally {
70
+ setBusy(false)
71
+ }
72
+ }
73
+
74
+ const handleSave = async (): Promise<void> => {
75
+ setBusy(true)
76
+ setError('')
77
+ try {
78
+ const result = await api.world('save', draft)
79
+ onChanged(result.world)
80
+ } catch (err) {
81
+ setError((err as Error).message)
82
+ } finally {
83
+ setBusy(false)
84
+ }
85
+ }
86
+
87
+ const setRealms = (realms: WorldRealm[]): void => setDraft(prev => ({ ...prev, realms }))
88
+ const setRegions = (regions: WorldRegion[]): void => setDraft(prev => ({ ...prev, regions }))
89
+ const setFactions = (factions: WorldFaction[]): void => setDraft(prev => ({ ...prev, factions }))
90
+
91
+ return (
92
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-12)' }}>
93
+ {error !== '' && <div className={css.card} style={{ borderColor: 'var(--nf-error)' }}><span style={{ color: 'var(--nf-error)' }}>{error}</span></div>}
94
+
95
+ <div className={css.card}>
96
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
97
+ <span className={css.cardTitle}>大世界</span>
98
+ <div className={css.row}>
99
+ <button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={busy} onClick={() => { void handleGenerate() }}>
100
+ 提炼
101
+ </button>
102
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={busy} onClick={() => { void handleSave() }}>
103
+ 保存
104
+ </button>
105
+ </div>
106
+ </div>
107
+ <span className={css.meta}>
108
+ 境界体系按由低到高排序注入章节生成提示词,模型不得随意跳级或自创境界;区域与势力约束地理/势力设定。提炼不满意可逐条编辑后保存。
109
+ </span>
110
+ </div>
111
+
112
+ {/* 境界体系 */}
113
+ <div className={css.card}>
114
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
115
+ <span className={css.cardTitle}>境界体系({draft.realms.length})</span>
116
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRealms([...draft.realms, { name: '', description: '' }]) }}>
117
+ + 新增境界
118
+ </button>
119
+ </div>
120
+ {draft.realms.length === 0 ? (
121
+ <span className={css.meta}>暂无境界体系 — 点击 提炼 或手动添加(由低到高)。</span>
122
+ ) : (
123
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
124
+ {draft.realms.map((realm, i) => (
125
+ <EditableRow
126
+ key={i}
127
+ name={realm.name}
128
+ detail={realm.description}
129
+ onName={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, name: v }; setRealms(next) }}
130
+ onDetail={v => { const next = [...draft.realms]; next[i] = { ...next[i]!, description: v }; setRealms(next) }}
131
+ onRemove={() => { setRealms(draft.realms.filter((_, idx) => idx !== i)) }}
132
+ namePlaceholder={`第 ${i + 1} 阶境界名`}
133
+ detailPlaceholder="突破条件 / 寿命 / 标志…"
134
+ />
135
+ ))}
136
+ </div>
137
+ )}
138
+ </div>
139
+
140
+ {/* 地理区域 */}
141
+ <div className={css.card}>
142
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
143
+ <span className={css.cardTitle}>地理区域({draft.regions.length})</span>
144
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setRegions([...draft.regions, { name: '', description: '' }]) }}>
145
+ + 新增区域
146
+ </button>
147
+ </div>
148
+ {draft.regions.length === 0 ? (
149
+ <span className={css.meta}>暂无地理区域 — 大陆 / 海域 / 秘境 / 遗迹…</span>
150
+ ) : (
151
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
152
+ {draft.regions.map((region, i) => (
153
+ <EditableRow
154
+ key={i}
155
+ name={region.name}
156
+ detail={region.description}
157
+ onName={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, name: v }; setRegions(next) }}
158
+ onDetail={v => { const next = [...draft.regions]; next[i] = { ...next[i]!, description: v }; setRegions(next) }}
159
+ onRemove={() => { setRegions(draft.regions.filter((_, idx) => idx !== i)) }}
160
+ namePlaceholder="区域名(如大荒 / 青云山脉)"
161
+ detailPlaceholder="描述…"
162
+ />
163
+ ))}
164
+ </div>
165
+ )}
166
+ </div>
167
+
168
+ {/* 势力分布 */}
169
+ <div className={css.card}>
170
+ <div className={css.row} style={{ justifyContent: 'space-between' }}>
171
+ <span className={css.cardTitle}>势力分布({draft.factions.length})</span>
172
+ <button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={() => { setFactions([...draft.factions, { name: '', kind: '宗门', description: '' }]) }}>
173
+ + 新增势力
174
+ </button>
175
+ </div>
176
+ {draft.factions.length === 0 ? (
177
+ <span className={css.meta}>暂无势力 — 宗门 / 家族 / 王朝 / 组织…</span>
178
+ ) : (
179
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)' }}>
180
+ {draft.factions.map((faction, i) => (
181
+ <EditableRow
182
+ key={i}
183
+ name={`${faction.name}(${faction.kind})`}
184
+ detail={faction.description}
185
+ onName={v => {
186
+ const next = [...draft.factions]
187
+ const match = /^(.*)((.*))$/.exec(v)
188
+ next[i] = { ...next[i]!, name: (match?.[1] ?? v).trim(), kind: (match?.[2] ?? next[i]!.kind).trim() || '宗门' }
189
+ setFactions(next)
190
+ }}
191
+ onDetail={v => { const next = [...draft.factions]; next[i] = { ...next[i]!, description: v }; setFactions(next) }}
192
+ onRemove={() => { setFactions(draft.factions.filter((_, idx) => idx !== i)) }}
193
+ namePlaceholder="势力名(类型)如:青云宗(宗门)"
194
+ detailPlaceholder="描述…"
195
+ />
196
+ ))}
197
+ </div>
198
+ )}
199
+ </div>
200
+ </div>
201
+ )
202
+ }
@@ -1,13 +1,15 @@
1
1
  /**
2
- * Tiny translation helper for the panel: reads the zh dict with the en dict
3
- * as fallback (the family plugins use a full locale registry; the panel keeps
4
- * a dependency-free helper so the client bundle stays self-contained).
2
+ * Tiny translation helper for the panel.
3
+ *
4
+ * 面板是中文单语界面:文案只在 `locales.ts` 定义一次,这里按类型化键位取值
5
+ * (键位不匹配会在编译期报错),保持客户端 bundle 自包含、不引入运行时语言服务。
6
+ * 需要多语言时再接入 DSH 的 locale 服务,届时只需替换本函数实现。
5
7
  */
6
- import { zh, en, type NovelKey } from '../locales.ts'
8
+ import { zh, type NovelKey } from '../locales.ts'
7
9
 
8
10
  /** Translate one key with optional {placeholder} substitution. */
9
11
  export function tt(key: NovelKey, params?: Record<string, string | number>): string {
10
- let text: string = zh[key] ?? en[key] ?? key
12
+ let text: string = zh[key] ?? key
11
13
  if (params !== undefined) {
12
14
  for (const [name, value] of Object.entries(params)) {
13
15
  text = text.replaceAll(`{${name}}`, String(value))