@waterwx/dsh-novel-forge 1.3.2 → 1.3.3-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 (172) hide show
  1. package/README.md +288 -279
  2. package/lib/client.js +56288 -12061
  3. package/lib/index.js +14655 -5573
  4. package/lib/types/action-guard.d.ts +61 -0
  5. package/lib/types/ai-scan.d.ts +30 -0
  6. package/lib/types/assets.d.ts +22 -9
  7. package/lib/types/assistant.d.ts +29 -0
  8. package/lib/types/author-assets.d.ts +23 -0
  9. package/lib/types/bookshelf.d.ts +18 -3
  10. package/lib/types/client/api.d.ts +187 -10
  11. package/lib/types/client/chip-prefs.d.ts +36 -0
  12. package/lib/types/client/docx.d.ts +2 -2
  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 +29 -67
  16. package/lib/types/client/mount.d.ts +0 -2
  17. package/lib/types/client/panel/AdaptModeView.d.ts +5 -0
  18. package/lib/types/client/panel/AiTag.d.ts +1 -0
  19. package/lib/types/client/panel/AssistantTab.d.ts +4 -1
  20. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  21. package/lib/types/client/panel/AuthorHome.d.ts +34 -0
  22. package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
  23. package/lib/types/client/panel/CmdPalette.d.ts +12 -0
  24. package/lib/types/client/panel/CreateBookView.d.ts +5 -1
  25. package/lib/types/client/panel/DirectorView.d.ts +7 -0
  26. package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +4 -0
  27. package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
  28. package/lib/types/client/panel/ImportModal.d.ts +7 -0
  29. package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
  30. package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
  31. package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
  32. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  33. package/lib/types/client/panel/NovelPanel.d.ts +6 -1
  34. package/lib/types/client/panel/PageHeader.d.ts +18 -0
  35. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  36. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  37. package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
  38. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  39. package/lib/types/client/panel/SettingsView.d.ts +19 -0
  40. package/lib/types/client/panel/ShelfView.d.ts +12 -1
  41. package/lib/types/client/panel/SubPage.d.ts +27 -0
  42. package/lib/types/client/panel/TensionView.d.ts +18 -0
  43. package/lib/types/client/panel/TimelineView.d.ts +18 -0
  44. package/lib/types/client/panel/helpers.d.ts +5 -3
  45. package/lib/types/client/panel/market-radar-state.d.ts +56 -0
  46. package/lib/types/client/panel/toast.d.ts +2 -0
  47. package/lib/types/client/panel/views.d.ts +21 -35
  48. package/lib/types/client/run-status.d.ts +21 -0
  49. package/lib/types/client/sidebar-entry.d.ts +0 -2
  50. package/lib/types/client/text.d.ts +9 -0
  51. package/lib/types/docx-core.d.ts +16 -0
  52. package/lib/types/docx.d.ts +2 -3
  53. package/lib/types/engine.d.ts +186 -45
  54. package/lib/types/global-assets.d.ts +6 -0
  55. package/lib/types/home.d.ts +11 -0
  56. package/lib/types/index.d.ts +36 -10
  57. package/lib/types/llm-live.d.ts +70 -0
  58. package/lib/types/llm-retry.d.ts +38 -0
  59. package/lib/types/market-radar-scan.d.ts +18 -0
  60. package/lib/types/market-radar-sources.d.ts +31 -0
  61. package/lib/types/migrate-output.d.ts +25 -0
  62. package/lib/types/model-capability.d.ts +93 -0
  63. package/lib/types/novel-context.d.ts +80 -0
  64. package/lib/types/output-budget.d.ts +86 -0
  65. package/lib/types/prompt-slots.d.ts +57 -0
  66. package/lib/types/prompt-utils.d.ts +35 -0
  67. package/lib/types/prompting.d.ts +10 -0
  68. package/lib/types/protocol.d.ts +1378 -166
  69. package/lib/types/review-policy.d.ts +97 -0
  70. package/lib/types/revision.d.ts +76 -0
  71. package/lib/types/routes.d.ts +9 -3
  72. package/lib/types/run.d.ts +23 -1
  73. package/lib/types/settings-persist.d.ts +59 -0
  74. package/lib/types/snapshots.d.ts +46 -0
  75. package/lib/types/stage-contract.d.ts +58 -0
  76. package/lib/types/tension.d.ts +53 -0
  77. package/lib/types/timeline.d.ts +37 -0
  78. package/package.json +54 -28
  79. package/src/action-guard.ts +127 -0
  80. package/src/ai-scan.ts +155 -0
  81. package/src/assets.ts +1801 -520
  82. package/src/assistant.ts +1098 -770
  83. package/src/author-assets.ts +131 -0
  84. package/src/bookshelf.ts +193 -151
  85. package/src/client/api.ts +380 -23
  86. package/src/client/chip-prefs.ts +121 -0
  87. package/src/client/docx.ts +32 -69
  88. package/src/client/index.ts +3 -1
  89. package/src/client/llmLive.ts +111 -0
  90. package/src/client/locales.ts +29 -253
  91. package/src/client/mount.tsx +4 -3
  92. package/src/client/panel/AdaptModeView.tsx +535 -0
  93. package/src/client/panel/AiTag.tsx +7 -0
  94. package/src/client/panel/AssetsTab.tsx +448 -458
  95. package/src/client/panel/AssistantTab.tsx +355 -301
  96. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  97. package/src/client/panel/AuthorHome.tsx +309 -0
  98. package/src/client/panel/BookAnalysisView.tsx +63 -0
  99. package/src/client/panel/CmdPalette.tsx +102 -0
  100. package/src/client/panel/CreateBookView.tsx +366 -348
  101. package/src/client/panel/DirectorView.tsx +200 -0
  102. package/src/client/panel/GlobalAssetLibraryView.tsx +168 -0
  103. package/src/client/panel/IdeaInspirationView.tsx +76 -0
  104. package/src/client/panel/ImportModal.tsx +303 -0
  105. package/src/client/panel/KnowledgeBaseView.tsx +125 -0
  106. package/src/client/panel/LiveFeedLog.tsx +204 -0
  107. package/src/client/panel/MarketRadarView.tsx +415 -0
  108. package/src/client/panel/ModelManager.tsx +205 -0
  109. package/src/client/panel/NovelPanel.tsx +3013 -2500
  110. package/src/client/panel/PageHeader.tsx +37 -0
  111. package/src/client/panel/ProgressConsole.tsx +63 -0
  112. package/src/client/panel/PromptSlotsView.tsx +97 -0
  113. package/src/client/panel/ReaderView.tsx +306 -306
  114. package/src/client/panel/ReasoningSection.tsx +31 -0
  115. package/src/client/panel/RunPanel.tsx +64 -32
  116. package/src/client/panel/SettingsView.tsx +355 -0
  117. package/src/client/panel/ShelfView.tsx +266 -236
  118. package/src/client/panel/SubPage.tsx +42 -0
  119. package/src/client/panel/TensionView.tsx +212 -0
  120. package/src/client/panel/TimelineView.tsx +208 -0
  121. package/src/client/panel/WorldTab.tsx +202 -202
  122. package/src/client/panel/helpers.ts +7 -5
  123. package/src/client/panel/market-radar-state.ts +79 -0
  124. package/src/client/panel/panel.module.css +4481 -1655
  125. package/src/client/panel/toast.ts +51 -0
  126. package/src/client/panel/views.tsx +316 -314
  127. package/src/client/run-status.ts +171 -0
  128. package/src/client/sidebar-entry.ts +4 -4
  129. package/src/client/text.ts +28 -0
  130. package/src/docx-core.ts +53 -0
  131. package/src/docx.ts +44 -83
  132. package/src/engine.ts +4759 -2943
  133. package/src/global-assets.ts +58 -0
  134. package/src/home.ts +33 -0
  135. package/src/index.ts +206 -65
  136. package/src/llm-live.ts +245 -0
  137. package/src/llm-retry.ts +77 -0
  138. package/src/market-radar-scan.ts +35 -0
  139. package/src/market-radar-sources.ts +173 -0
  140. package/src/migrate-output.ts +76 -0
  141. package/src/model-capability.ts +215 -0
  142. package/src/novel-context.ts +286 -0
  143. package/src/output-budget.ts +147 -0
  144. package/src/prompt-slots.ts +125 -0
  145. package/src/prompting.ts +26 -0
  146. package/src/protocol.ts +1405 -158
  147. package/src/revision.ts +283 -0
  148. package/src/routes.ts +1959 -436
  149. package/src/run.ts +118 -21
  150. package/src/settings-persist.ts +114 -0
  151. package/src/snapshots.ts +174 -0
  152. package/src/stage-contract.ts +186 -0
  153. package/src/tension.ts +251 -0
  154. package/src/timeline.ts +159 -0
  155. package/lib/client.js.map +0 -1
  156. package/lib/index.js.map +0 -1
  157. package/lib/types/comic-presets.d.ts +0 -12
  158. package/scripts/assistant-frames.txt +0 -8
  159. package/scripts/diagnose-adapter.mts +0 -81
  160. package/scripts/diagnose-assistant.mjs +0 -58
  161. package/scripts/diagnose-bible.mjs +0 -72
  162. package/scripts/diagnose-harness.mts +0 -75
  163. package/scripts/novel-forge-restart.log +0 -10
  164. package/scripts/novel-forge-web.stderr.log +0 -2
  165. package/scripts/novel-forge-web.stdout.log +0 -1
  166. package/scripts/outline-sample.txt +0 -449
  167. package/scripts/probe.mjs +0 -6
  168. package/scripts/restart-web.ps1 +0 -88
  169. package/scripts/smoke.mts +0 -72
  170. package/scripts/upload-github.ps1 +0 -51
  171. package/src/client/panel/BookshelfBar.tsx +0 -170
  172. package/src/comic-presets.ts +0 -67
@@ -1,69 +1,32 @@
1
- /**
2
- * Browser-side docx outline extraction: a .docx is a zip whose
3
- * word/document.xml holds the body text in <w:t> runs inside <w:p> paragraphs.
4
- * Uses fflate (inlined into the client bundle) so the user can pick or drag a
5
- * docx without any server upload.
6
- *
7
- * Import from 'fflate/browser' (not 'fflate'): the default entry resolves to
8
- * the Node build (esm/index.mjs), which calls module.createRequire() for the
9
- * optional worker_threads path — inlining that into the browser bundle leaves
10
- * a bare require("module") the client-modules table cannot answer.
11
- */
12
-
13
- import { unzipSync, strFromU8 } from 'fflate/browser'
14
-
15
- /** Decode the handful of XML entities docx bodies actually use. */
16
- function decodeEntities(text: string): string {
17
- return text
18
- .replace(/&lt;/g, '<')
19
- .replace(/&gt;/g, '>')
20
- .replace(/&quot;/g, '"')
21
- .replace(/&apos;/g, "'")
22
- .replace(/&amp;/g, '&')
23
- .replace(/&nbsp;/g, ' ')
24
- }
25
-
26
- /** Extract plain text from a docx buffer: one line per <w:p> paragraph. */
27
- export function extractDocxTextFromBuffer(buffer: ArrayBuffer | Uint8Array): string {
28
- const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
29
- let files: ReturnType<typeof unzipSync>
30
- try {
31
- files = unzipSync(bytes)
32
- } catch (error) {
33
- throw new Error(`不是有效的 docx(zip 解压失败):${(error as Error).message}`)
34
- }
35
- const document = files['word/document.xml']
36
- if (document === undefined) {
37
- throw new Error('不是有效的 docx(缺少 word/document.xml)')
38
- }
39
- const xml = strFromU8(document)
40
-
41
- const paragraphs: string[] = []
42
- const parts = xml.split(/<w:p\b[^>]*>/)
43
- for (let i = 1; i < parts.length; i++) {
44
- const segment = parts[i]!
45
- const runs: string[] = []
46
- const runRe = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>|<w:tab\b[^>]*\/>|<w:br\b[^>]*\/>/g
47
- for (const match of segment.matchAll(runRe)) {
48
- if (match[0].startsWith('<w:tab')) {
49
- runs.push('\t')
50
- } else if (match[0].startsWith('<w:br')) {
51
- runs.push('\n')
52
- } else {
53
- runs.push(decodeEntities(match[1] ?? ''))
54
- }
55
- }
56
- paragraphs.push(runs.join('').replace(/\u00a0/g, ' ').trimEnd())
57
- }
58
-
59
- const text = paragraphs.join('\n').replace(/\n{3,}/g, '\n\n').trim()
60
- if (text.length === 0) {
61
- throw new Error('docx 中没有可提取的文本')
62
- }
63
- return text
64
- }
65
-
66
- /** Read a File as ArrayBuffer. */
67
- export function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
68
- return file.arrayBuffer()
69
- }
1
+ /**
2
+ * Browser-side docx outline extraction: a .docx is a zip whose
3
+ * word/document.xml holds the body text in <w:t> runs inside <w:p> paragraphs.
4
+ * Uses fflate (inlined into the client bundle) so the user can pick or drag a
5
+ * docx without any server upload.
6
+ *
7
+ * Import from 'fflate/browser' (not 'fflate'): the default entry resolves to
8
+ * the Node build (esm/index.mjs), which calls module.createRequire() for the
9
+ * optional worker_threads path — inlining that into the browser bundle leaves
10
+ * a bare require("module") the client-modules table cannot answer.
11
+ *
12
+ * XML 解析共享核心在 ../docx-core.ts(与服务端同源)。
13
+ */
14
+
15
+ import { unzipSync, strFromU8 } from 'fflate/browser'
16
+ import { extractDocxTextFromXml } from '../docx-core.ts'
17
+
18
+ /** Extract plain text from a docx buffer: one line per <w:p> paragraph. */
19
+ export function extractDocxTextFromBuffer(buffer: ArrayBuffer | Uint8Array): string {
20
+ const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
21
+ let files: ReturnType<typeof unzipSync>
22
+ try {
23
+ files = unzipSync(bytes)
24
+ } catch (error) {
25
+ throw new Error(`不是有效的 docx(zip 解压失败):${(error as Error).message}`)
26
+ }
27
+ const document = files['word/document.xml']
28
+ if (document === undefined) {
29
+ throw new Error('不是有效的 docx(缺少 word/document.xml)')
30
+ }
31
+ return extractDocxTextFromXml(strFromU8(document))
32
+ }
@@ -4,11 +4,12 @@
4
4
  * problems are logged, never thrown — the web shell fails the whole boot when
5
5
  * a plugin apply throws.
6
6
  */
7
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
7
+ import type { Context as ClientContext } from '@deepseek-ai/cordis'
8
8
  import { NovelApi } from './api.ts'
9
9
  import { mountPanel } from './mount.tsx'
10
10
  import { PanelController } from './panel/controller.ts'
11
11
  import { mountSidebarEntry } from './sidebar-entry.ts'
12
+ import { mountRunStatus } from './run-status.ts'
12
13
 
13
14
  /** Required services (fiber inject waiting). */
14
15
  export const inject = ['slots', 'locale']
@@ -24,6 +25,7 @@ export function apply(ctx: ClientContext): void {
24
25
  try {
25
26
  disposers.push(mountSidebarEntry(controller))
26
27
  disposers.push(mountPanel(controller, api))
28
+ disposers.push(mountRunStatus(controller, api))
27
29
  } catch (error) {
28
30
  // DOM failures degrade the panel, never the GUI.
29
31
  console.warn('[dsh-novel-forge] mount failed:', error)
@@ -0,0 +1,111 @@
1
+ /**
2
+ * 浏览器端「AI 创作实况」钩子:连接 /llm-live/stream (SSE),按 LLM 调用分组会话。
3
+ * 轻量实现,不持久化缓存(对齐上游 useLlmLiveFeed 的核心行为)。
4
+ */
5
+
6
+ import { useEffect, useMemo, useState } from 'react'
7
+ import { NOVEL_API, type LlmLiveFrame, type LlmTokenUsage } from '../protocol.ts'
8
+
9
+ // 帧结构定义在 protocol.ts(宿主与浏览器共用同一份线上形状),此处只再导出。
10
+ export type { LlmLiveFrame }
11
+
12
+ export interface LlmLiveSession {
13
+ sessionId: string
14
+ label: string
15
+ model?: string
16
+ phase: 'requesting' | 'streaming' | 'completed' | 'failed'
17
+ phaseMessage: string
18
+ preview: string
19
+ totalChars: number
20
+ startedAt: string
21
+ updatedAt: string
22
+ /** 本次调用的 token 用量(供应商未上报时缺省)。 */
23
+ usage?: LlmTokenUsage
24
+ /** 本次调用耗时(毫秒)。 */
25
+ elapsedMs?: number
26
+ /** 首字耗时(毫秒)。 */
27
+ firstTokenMs?: number
28
+ /** 是否保存了可查看的 Prompt。 */
29
+ hasPrompt?: boolean
30
+ /** Prompt 字符数(system + user)。 */
31
+ promptChars?: number
32
+ }
33
+
34
+ const MAX_PREVIEW_CHARS = 20_000
35
+
36
+ function applyFrame(current: Record<string, LlmLiveSession>, frame: LlmLiveFrame): Record<string, LlmLiveSession> {
37
+ const next = { ...current }
38
+ const id = frame.sessionId
39
+ let session = next[id]
40
+ if (frame.type === 'session_started') {
41
+ session = {
42
+ sessionId: id,
43
+ label: frame.label ?? 'LLM 调用',
44
+ model: frame.model,
45
+ phase: 'requesting',
46
+ phaseMessage: '正在连接模型',
47
+ preview: '',
48
+ totalChars: 0,
49
+ startedAt: frame.at,
50
+ updatedAt: frame.at,
51
+ hasPrompt: frame.hasPrompt,
52
+ promptChars: frame.promptChars,
53
+ }
54
+ next[id] = session
55
+ return next
56
+ }
57
+ if (session === undefined) return next
58
+ if (frame.type === 'output_delta') {
59
+ const preview = (session.preview + (frame.content ?? '')).slice(-MAX_PREVIEW_CHARS)
60
+ session = { ...session, phase: session.phase === 'requesting' ? 'streaming' : session.phase, phaseMessage: session.phase === 'requesting' ? '模型正在返回内容' : session.phaseMessage, preview, totalChars: frame.totalChars ?? session.totalChars + (frame.content?.length ?? 0), updatedAt: frame.at }
61
+ next[id] = session
62
+ } else if (frame.type === 'reasoning_delta') {
63
+ session = { ...session, phase: session.phase === 'requesting' ? 'streaming' : session.phase, updatedAt: frame.at }
64
+ next[id] = session
65
+ } else if (frame.type === 'phase_changed') {
66
+ session = { ...session, phase: frame.phase ?? session.phase, phaseMessage: frame.phaseMessage ?? session.phaseMessage, updatedAt: frame.at }
67
+ next[id] = session
68
+ } else if (frame.type === 'session_completed') {
69
+ session = {
70
+ ...session,
71
+ phase: frame.phase ?? 'completed',
72
+ phaseMessage: frame.phase === 'failed' ? (frame.error ?? '调用失败') : '模型结果已准备完成',
73
+ totalChars: frame.totalChars ?? session.totalChars,
74
+ preview: session.preview || (frame.preview ?? ''),
75
+ updatedAt: frame.at,
76
+ usage: frame.usage ?? session.usage,
77
+ elapsedMs: frame.elapsedMs ?? session.elapsedMs,
78
+ firstTokenMs: frame.firstTokenMs ?? session.firstTokenMs,
79
+ }
80
+ next[id] = session
81
+ }
82
+ return next
83
+ }
84
+
85
+ export function useLlmLiveFeed(enabled = true): { connected: boolean; sessions: LlmLiveSession[] } {
86
+ const [sessions, setSessions] = useState<Record<string, LlmLiveSession>>({})
87
+ const [connected, setConnected] = useState(false)
88
+ useEffect(() => {
89
+ if (!enabled) {
90
+ setSessions({})
91
+ setConnected(false)
92
+ return
93
+ }
94
+ const es = new EventSource(NOVEL_API.llmLive)
95
+ es.onopen = () => setConnected(true)
96
+ es.onmessage = (ev) => {
97
+ try {
98
+ const frame = JSON.parse(ev.data as string) as LlmLiveFrame
99
+ setSessions(prev => applyFrame(prev, frame))
100
+ } catch { /* ignore malformed frame */ }
101
+ }
102
+ // EventSource 断线会自动重连;这里只在断线瞬间标记未连接,重连成功后会再次 onopen。
103
+ es.onerror = () => setConnected(false)
104
+ return () => {
105
+ es.close()
106
+ setConnected(false)
107
+ }
108
+ }, [enabled])
109
+ const list = useMemo(() => Object.values(sessions).sort((a, b) => a.startedAt.localeCompare(b.startedAt)), [sessions])
110
+ return { connected, sessions: list }
111
+ }
@@ -1,58 +1,37 @@
1
1
  /**
2
- * dsh-novel-forge — locale dictionaries (zh / en).
2
+ * dsh-novel-forge — 面板文案词典(简体中文单语)。
3
+ *
4
+ * 定位说明:小说工坊面板是**中文单语界面**——这里就是全部用户可见文案的唯一定义处,
5
+ * 键位即类型(`NovelKey`),新增文案请同时在此登记,不要散落到组件里硬编码。
6
+ * 文档层(README / docs / CHANGELOG)仍提供英文对照,那是给人读的,不是界面语言。
3
7
  */
4
8
 
5
- /** zh dictionary. */
9
+ /** 面板文案(简体中文)。 */
6
10
  export const zh = {
7
11
  'entry.label': '小说工坊',
8
12
  'entry.tooltip': 'AI 编译小说工作台:总纲 → 道藏 → 卷计划 → 章节计划 → 逐章生成+审稿',
9
- 'panel.title': '小说工坊',
10
13
  'common.close': '关闭',
11
14
  'common.loading': '加载中…',
12
- 'common.save': '保存',
13
15
  'common.error': '错误',
14
- 'common.success': '成功',
15
- 'common.generating': '生成中…',
16
16
  'common.chars': '字',
17
17
  'tab.workflow': '工作台',
18
18
  'tab.overview': '总纲',
19
19
  'tab.plan': '章节',
20
- 'tab.bible': '道藏',
21
- 'tab.foreshadow': '暗线',
22
- 'tab.assistant': 'AI 助手',
20
+ 'tab.assistant': 'AI 编辑 Agent',
23
21
  'tab.settings': '设置',
24
- 'workflow.title': '创作工作台',
25
- 'workflow.step1': '① 加载总纲',
26
- 'workflow.step2': '② 提炼道藏',
27
- 'workflow.step3': '③ 规划卷',
28
- 'workflow.step4': '④ 生成章节计划',
29
- 'workflow.step5': '⑤ 逐章写作 + AI 审稿',
30
- 'workflow.step6': '⑥ 润色 / 导出',
31
- 'workflow.loadOutline': '读取总纲',
32
- 'workflow.genBible': '提炼道藏',
33
22
  'workflow.genVolumes': '生成卷计划',
34
- 'workflow.genPlan': '生成章节计划',
35
- 'workflow.done': '已完成',
36
- 'workflow.todo': '待办',
37
23
  'workflow.bibleDone': '道藏已生成({n} 条规则 / {c} 个人物 / {r} 条红线)',
38
24
  'workflow.volumesDone': '卷计划已生成({n} 卷)',
39
25
  'workflow.planDone': '章节计划已生成({n} 章)',
40
- 'workflow.progress': '进度:总纲 ✓ · 道藏 {bible} · 卷 {volumes} · 计划 {plan} · 已完成 {done}/{total} 章',
41
- 'overview.loadDocx': '从 docx 读取总纲',
42
- 'overview.loadDocxDefault': '读取默认总纲',
43
26
  'overview.loadingOutline': '正在解析 docx…',
44
27
  'overview.outlineHint': '总纲文本(可编辑)',
45
28
  'overview.outlineChars': '总纲字数',
46
29
  'overview.saveOutline': '保存总纲',
47
30
  'overview.saved': '总纲已保存',
48
31
  'overview.bookName': '书名',
49
- 'overview.loadCustom': '指定 docx 路径',
50
- 'overview.loadCustomHint': '绝对路径,留空使用默认',
51
32
  'plan.generate': '生成章节计划',
52
33
  'plan.generateHint': '章节数量',
53
34
  'plan.count': '章',
54
- 'plan.empty': '暂无章节计划,请先生成',
55
- 'plan.chapter': '章',
56
35
  'plan.pending': '待生成',
57
36
  'plan.generating': '生成中',
58
37
  'plan.written': '待审稿',
@@ -64,23 +43,17 @@ export const zh = {
64
43
  'plan.rewrite': '修订',
65
44
  'plan.review': '审稿',
66
45
  'plan.polish': '去AI味',
67
- 'plan.writeAll': '批量生成全部',
68
46
  'plan.writeAllPending': '批量生成剩余',
69
- 'plan.generated': '已生成',
70
- 'plan.progress': '进度',
71
47
  'plan.beats': '剧情要点',
72
48
  'plan.reviewReport': '审稿报告',
73
49
  'plan.reviewScore': '评分',
74
50
  'plan.reviewVerdict': '总评',
75
- 'plan.reviewIssues': '问题清单',
76
51
  'plan.reviewPass': '通过',
77
52
  'plan.reviewFail': '未通过',
78
- 'plan.approve': '手动通过',
79
53
  'plan.summary': '章节摘要',
80
54
  'plan.volumes': '卷',
81
- 'plan.noVolume': '未分卷',
82
55
  'bible.title': '道藏',
83
- 'bible.gen': 'AI 提炼道藏',
56
+ 'bible.gen': '提炼道藏',
84
57
  'bible.genre': '题材基调',
85
58
  'bible.worldRules': '世界规则',
86
59
  'bible.characters': '人物志',
@@ -88,9 +61,8 @@ export const zh = {
88
61
  'bible.style': '风格要求',
89
62
  'bible.none': '尚未生成道藏。生成后写作会严格遵守人设与金手指规则,审稿也会按红线检查。',
90
63
  'foreshadow.title': '暗线管理',
91
- 'foreshadow.suggest': 'AI 建议暗线',
64
+ 'foreshadow.suggest': '建议暗线',
92
65
  'foreshadow.none': '暂无暗线',
93
- 'foreshadow.status': '状态',
94
66
  'foreshadow.planned': '计划中',
95
67
  'foreshadow.planted': '已埋设',
96
68
  'foreshadow.progressing': '推进中',
@@ -100,7 +72,6 @@ export const zh = {
100
72
  'foreshadow.plantedAt': '埋设于',
101
73
  'foreshadow.setPlanted': '标记已埋设',
102
74
  'foreshadow.setResolved': '标记已回收',
103
- 'tab.facts': '编年录',
104
75
  'tab.plotlines': '剧情线',
105
76
  'plotlines.hint': '主线/支线/人物线/悬念线的推进管理:章节生成时会注入未完结的线,要求每章至少推进一条;关联章节记录推进节点。',
106
77
  'plotlines.empty': '还没有剧情线——长篇小说建议先建主线与几条支线。',
@@ -122,44 +93,42 @@ export const zh = {
122
93
  'plotlines.cancel': '取消',
123
94
  'plotlines.edit': '编辑',
124
95
  'plotlines.remove': '删除',
125
- 'plotlines.linkChapter': '关联本章',
126
96
  'plotlines.chapters': '已关联',
127
- 'plotlines.workflowTitle': '剧情线进度',
128
- 'plotlines.workflowEmpty': '未建剧情线(去「剧情线」页创建)',
129
- 'sensitive.title': '敏感词检查',
130
- 'sensitive.scanAll': '🔞 全书敏感词检查',
97
+ 'sensitive.scanAll': ' 全书敏感词检查',
131
98
  'sensitive.hint': '内置违禁词库硬匹配(政治/擦边/暴力/辱骂/广告/其他),发布前建议检查;只做提示,是否修改由你判断。',
132
- 'sensitive.clean': '✅ 未命中任何违禁词(扫描 {n} 章)',
99
+ 'sensitive.clean': ' 未命中任何违禁词(扫描 {n} 章)',
133
100
  'sensitive.hits': '命中 {n} 处({chapters} 章受影响)',
134
101
  'sensitive.goFix': '去修订',
135
102
  'sensitive.fixPrefill': '请删除或替换正文中的违禁表达「{word}」(类别:{category}),用含蓄/间接的写法替代。',
136
103
  'facts.title': '编年录 / 时间线({n} 条)',
137
104
  'facts.hint': '每章生成后自动抽取「已确立事实」(人物状态/境界资源/关系变化/伏笔落地),最近 20 条注入后续章节生成,保证长期一致。',
138
105
  'facts.backfill': '回填历史章节事实',
139
- 'facts.backfilled': '已从历史章节回填 {n} 条事实',
140
- 'settings.title': '设置',
141
- 'settings.outlinePath': '默认大纲路径',
142
106
  'settings.outputDir': '输出目录',
143
107
  'settings.provider': '模型提供商',
144
108
  'settings.model': '模型',
145
109
  'settings.modelCustom': '自定义…',
110
+ 'settings.customProvider': '自定义提供商…',
111
+ 'settings.edit': '编辑',
112
+ 'settings.delete': '删除',
146
113
  'settings.modelCustomPlaceholder': '输入任意模型 id',
147
- 'settings.reasoningEffort': '思考强度 (reasoningEffort)',
114
+ 'settings.refreshModels': '刷新模型',
115
+ 'settings.testConnection': '测试连通',
116
+ 'settings.testRunning': '测试中',
117
+ 'settings.testOk': '连通',
118
+ 'settings.testFail': '失败',
119
+ 'settings.testHint': '真实发送一次最小调用(约几十 token),验证 Key / 端点 / 模型可用。',
120
+ 'settings.reasoningSection': '推理强度',
121
+ 'settings.reasoningEffort': '写作类任务思考强度 (reasoningEffort)',
122
+ 'settings.reasoningHint': '章节生成/润色/重写/审稿等写作任务使用;关掉更省更快。',
123
+ 'settings.analysisReasoning': '分析类任务思考强度(提炼/拆书/反推大纲)',
124
+ 'settings.analysisReasoningHint': '角色/场景/道藏提炼、拆书、反推大纲等一次性分析任务默认 low,保证质量,不受上面写作档位影响。',
148
125
  'settings.reasoning.off': 'off · 关闭思考(推荐日常写文)',
149
126
  'settings.reasoning.low': 'low · 低思考',
150
127
  'settings.reasoning.high': 'high · 高思考',
151
128
  'settings.reasoning.max': 'max · 最高思考(质检/推演)',
152
- 'settings.reasoningHint': 'off 最快最省;high/max 更慢更贵但推理更强',
153
- 'settings.theme': '主题',
154
- 'settings.themeLiquid': 'iOS 液态玻璃(绿)',
155
- 'settings.themeClassic': '经典毛玻璃(蓝)',
156
- 'settings.themeNeumorph': '新拟物(双阴影)',
157
- 'settings.themeHint': '存于浏览器本地,即时生效',
158
129
  'settings.chapterChars': '每章目标字数',
159
130
  'settings.maxTokens': '单章最大输出 tokens',
160
131
  'settings.reviewPassScore': '审稿通过分数(0-100)',
161
- 'settings.editorFontSize': '编辑器字号(仅编辑页显示)',
162
- 'settings.editorFontSizeHint': '存于浏览器本地,不影响其他设备',
163
132
  'settings.autoReview': '生成后自动审稿',
164
133
  'settings.autoAuthorReview': '生成后自动作者复盘',
165
134
  'settings.autoAuthorReviewHint': '复盘钩子兑现/结尾钩子/剧情线推进/连续性/节奏趋势,每章约 2000 token',
@@ -167,8 +136,8 @@ export const zh = {
167
136
  'settings.autoReviewAfterReviseHint': '按意见修订/润色产出草稿后自动附带一次 AI 审查,直接显示新稿评分与剩余问题(每章约 2-3k token,可关)',
168
137
  'settings.save': '保存设置',
169
138
  'settings.saved': '设置已保存',
139
+ 'settings.savedUnpersisted': '设置已生效,但未能持久化(宿主未登记本插件的配置段),重启后会回到原值。',
170
140
  'settings.openFolder': '打开输出文件夹',
171
- 'settings.export': '导出',
172
141
  'settings.exportTxt': '导出 TXT',
173
142
  'settings.exportMd': '导出 Markdown',
174
143
  'settings.exported': '已导出:{file}({chars} 字,{chapters} 章)',
@@ -178,206 +147,13 @@ export const zh = {
178
147
  'progress.error': '第 {no} 章失败:{message}',
179
148
  'progress.rewriting': '正在修订第 {no} 章…',
180
149
  'progress.polishing': '正在润色第 {no} 章…',
181
- 'progress.empty': '生成/审稿进度将显示在这里',
182
- 'assistant.hint': '和 AI 编辑讨论剧情、人设、暗线;达成一致后可让它直接修改总纲、道藏、章节内容。',
150
+ 'assistant.hint': '和 AI 编辑 Agent 讨论剧情、人设、暗线;达成一致后可让它直接修改总纲、道藏、章节内容。',
183
151
  'assistant.placeholder': '例如:我想让第 2 章结尾加一个悬念——墟境里传来爷爷的声音…',
184
152
  'assistant.send': '发送',
185
- 'assistant.toolStart': '⚙ 执行操作:{name}…',
186
- 'assistant.toolDone': '✓ {name} 完成:{detail}',
187
- 'assistant.toolError': '✗ {name} 失败:{detail}',
188
- 'assistant.empty': '还没有对话。和 AI 编辑聊聊剧情吧。',
153
+ 'assistant.empty': '还没有对话。和 AI 编辑 Agent 聊聊剧情吧。',
189
154
  'status.projectNone': '输出目录中还没有项目。请先加载大纲。',
190
155
  'status.files': '已生成文件',
191
- 'api.error': '请求失败',
192
156
  } as const
193
157
 
194
- /** en dictionary (fallback). */
195
- export const en: Record<string, string> = {
196
- 'entry.label': 'Novel Forge',
197
- 'entry.tooltip': 'AI novel workbench: outline → bible → volumes → plan → write + review',
198
- 'panel.title': 'Novel Forge',
199
- 'common.close': 'Close',
200
- 'common.loading': 'Loading…',
201
- 'common.save': 'Save',
202
- 'common.error': 'Error',
203
- 'common.success': 'Success',
204
- 'common.generating': 'Generating…',
205
- 'common.chars': ' chars',
206
- 'tab.workflow': 'Workbench',
207
- 'tab.overview': 'Outline',
208
- 'tab.plan': 'Chapters',
209
- 'tab.bible': 'Bible',
210
- 'tab.foreshadow': 'Foreshadow',
211
- 'tab.settings': 'Settings',
212
- 'workflow.title': 'Writing workbench',
213
- 'workflow.step1': '① Load outline',
214
- 'workflow.step2': '② Extract story bible',
215
- 'workflow.step3': '③ Plan volumes',
216
- 'workflow.step4': '④ Plan chapters',
217
- 'workflow.step5': '⑤ Write + AI review',
218
- 'workflow.step6': '⑥ Polish / export',
219
- 'workflow.loadOutline': 'Load outline',
220
- 'workflow.genBible': 'Extract bible',
221
- 'workflow.genVolumes': 'Plan volumes',
222
- 'workflow.genPlan': 'Plan chapters',
223
- 'workflow.done': 'done',
224
- 'workflow.todo': 'todo',
225
- 'workflow.bibleDone': 'Bible ready ({n} rules / {c} characters / {r} red lines)',
226
- 'workflow.volumesDone': 'Volumes ready ({n})',
227
- 'workflow.planDone': 'Plan ready ({n} chapters)',
228
- 'workflow.progress': 'Outline ✓ · bible {bible} · volumes {volumes} · plan {plan} · {done}/{total} chapters',
229
- 'overview.loadDocx': 'Load outline from docx',
230
- 'overview.loadDocxDefault': 'Load default outline',
231
- 'overview.loadingOutline': 'Parsing docx…',
232
- 'overview.outlineHint': 'Outline text (editable)',
233
- 'overview.outlineChars': 'Outline length',
234
- 'overview.saveOutline': 'Save outline',
235
- 'overview.saved': 'Outline saved',
236
- 'overview.bookName': 'Book',
237
- 'overview.loadCustom': 'Custom docx path',
238
- 'overview.loadCustomHint': 'Absolute path; empty = default',
239
- 'plan.generate': 'Plan chapters',
240
- 'plan.generateHint': 'Chapter count',
241
- 'plan.count': ' chapters',
242
- 'plan.empty': 'No plan yet — generate one first',
243
- 'plan.chapter': 'Ch.',
244
- 'plan.pending': 'pending',
245
- 'plan.generating': 'writing',
246
- 'plan.written': 'to review',
247
- 'plan.reviewing': 'reviewing',
248
- 'plan.approved': 'approved',
249
- 'plan.rejected': 'to revise',
250
- 'plan.error': 'failed',
251
- 'plan.write': 'Write',
252
- 'plan.rewrite': 'Revise',
253
- 'plan.review': 'Review',
254
- 'plan.polish': 'De-AI',
255
- 'plan.writeAll': 'Write all',
256
- 'plan.writeAllPending': 'Write remaining',
257
- 'plan.generated': 'generated',
258
- 'plan.progress': 'progress',
259
- 'plan.beats': 'Beats',
260
- 'plan.reviewReport': 'Review report',
261
- 'plan.reviewScore': 'Score',
262
- 'plan.reviewVerdict': 'Verdict',
263
- 'plan.reviewIssues': 'Issues',
264
- 'plan.reviewPass': 'Passed',
265
- 'plan.reviewFail': 'Failed',
266
- 'plan.approve': 'Approve',
267
- 'plan.summary': 'Summary',
268
- 'plan.volumes': 'Volumes',
269
- 'plan.noVolume': 'No volume',
270
- 'bible.title': 'Story bible',
271
- 'bible.gen': 'Extract bible with AI',
272
- 'bible.genre': 'Genre',
273
- 'bible.worldRules': 'World rules',
274
- 'bible.characters': 'Characters',
275
- 'bible.redLines': 'Red lines',
276
- 'bible.style': 'Style',
277
- 'bible.none': 'No bible yet. Generation and review follow it strictly once extracted.',
278
- 'foreshadow.title': 'Foreshadowing',
279
- 'foreshadow.suggest': 'Suggest with AI',
280
- 'foreshadow.none': 'No foreshadows',
281
- 'foreshadow.status': 'Status',
282
- 'foreshadow.planned': 'planned',
283
- 'foreshadow.planted': 'planted',
284
- 'foreshadow.progressing': 'progressing',
285
- 'foreshadow.resolved': 'resolved',
286
- 'foreshadow.abandoned': 'abandoned',
287
- 'foreshadow.target': 'target',
288
- 'foreshadow.plantedAt': 'planted at',
289
- 'foreshadow.setPlanted': 'Mark planted',
290
- 'foreshadow.setResolved': 'Mark resolved',
291
- 'settings.title': 'Settings',
292
- 'settings.outlinePath': 'Default outline path',
293
- 'settings.outputDir': 'Output directory',
294
- 'settings.provider': 'Provider',
295
- 'settings.model': 'Model',
296
- 'settings.modelCustom': 'Custom…',
297
- 'settings.modelCustomPlaceholder': 'Type any model id',
298
- 'settings.reasoningEffort': 'Reasoning effort',
299
- 'settings.reasoning.off': 'off · no thinking (recommended)',
300
- 'settings.reasoning.low': 'low',
301
- 'settings.reasoning.high': 'high',
302
- 'settings.reasoning.max': 'max (audit/reasoning)',
303
- 'settings.reasoningHint': 'off is fastest/cheapest; high/max are slower and stronger',
304
- 'settings.theme': 'Theme',
305
- 'settings.themeLiquid': 'iOS Liquid Glass (green)',
306
- 'settings.themeClassic': 'Classic frosted (blue)',
307
- 'settings.themeNeumorph': 'Neumorphism (dual-shadow)',
308
- 'settings.themeHint': 'Stored in browser localStorage; applies instantly',
309
- 'settings.chapterChars': 'Chars per chapter',
310
- 'settings.maxTokens': 'Max output tokens',
311
- 'settings.reviewPassScore': 'Review pass score (0-100)',
312
- 'settings.editorFontSize': 'Editor font size (workspace only)',
313
- 'settings.editorFontSizeHint': 'Stored in browser localStorage; display-only',
314
- 'settings.autoReview': 'Auto-review after writing',
315
- 'settings.autoAuthorReview': 'Auto author review after writing',
316
- 'settings.autoAuthorReviewHint': 'Reviews hook payoff / ending hook / plotline progress / continuity / pacing trend (~2000 tokens per chapter)',
317
- 'settings.autoReviewAfterRevise': 'Auto review after revise',
318
- 'settings.autoReviewAfterReviseHint': 'Automatically runs one AI review on the produced draft after revise/polish, showing the new score and remaining issues (~2-3k tokens, can be disabled)',
319
- 'settings.save': 'Save settings',
320
- 'settings.saved': 'Settings saved',
321
- 'settings.openFolder': 'Open output folder',
322
- 'settings.export': 'Export',
323
- 'settings.exportTxt': 'Export TXT',
324
- 'settings.exportMd': 'Export Markdown',
325
- 'settings.exported': 'Exported: {file} ({chars} chars, {chapters} chapters)',
326
- 'tab.facts': 'Chronicle',
327
- 'tab.plotlines': 'Plotlines',
328
- 'plotlines.hint': 'Track main/branch/character/mystery arcs: unfinished arcs are injected into chapter writing so each chapter advances at least one; linked chapters record progress nodes.',
329
- 'plotlines.empty': 'No plotlines yet — consider creating the main arc and a few branches.',
330
- 'plotlines.new': '+ New plotline',
331
- 'plotlines.name': 'Name',
332
- 'plotlines.kind': 'Type',
333
- 'plotlines.kindMain': 'Main',
334
- 'plotlines.kindBranch': 'Branch',
335
- 'plotlines.kindCharacter': 'Character',
336
- 'plotlines.kindMystery': 'Mystery',
337
- 'plotlines.goal': 'Goal',
338
- 'plotlines.progress': 'Current progress',
339
- 'plotlines.status': 'Status',
340
- 'plotlines.statusActive': 'Active',
341
- 'plotlines.statusPaused': 'Paused',
342
- 'plotlines.statusResolved': 'Resolved',
343
- 'plotlines.statusAbandoned': 'Abandoned',
344
- 'plotlines.save': 'Save',
345
- 'plotlines.cancel': 'Cancel',
346
- 'plotlines.edit': 'Edit',
347
- 'plotlines.remove': 'Remove',
348
- 'plotlines.linkChapter': 'Link chapter',
349
- 'plotlines.chapters': 'Linked',
350
- 'plotlines.workflowTitle': 'Plotline progress',
351
- 'plotlines.workflowEmpty': 'No plotlines yet (create them in the Plotlines tab)',
352
- 'sensitive.title': 'Sensitive-word check',
353
- 'sensitive.scanAll': '🔞 Scan all chapters',
354
- 'sensitive.hint': 'Built-in banned-word library (politics/erotica/violence/abuse/ads/other). Run before publishing; suggestions only.',
355
- 'sensitive.clean': '✅ No banned words found ({n} chapters scanned)',
356
- 'sensitive.hits': '{n} hits across {chapters} chapters',
357
- 'sensitive.goFix': 'Fix',
358
- 'sensitive.fixPrefill': 'Remove or rephrase the banned expression "{word}" (category: {category}) with an indirect alternative.',
359
- 'facts.title': 'Chronicle / timeline ({n} facts)',
360
- 'facts.hint': 'Auto-extracted established facts per chapter (state/resources/relations/foreshadow landings); the latest 20 are injected into later chapters for consistency.',
361
- 'facts.backfill': 'Backfill facts from historical chapters',
362
- 'facts.backfilled': 'Backfilled {n} facts from historical chapters',
363
- 'progress.generating': 'Writing chapter {no} “{title}”…',
364
- 'progress.done': 'Chapter {no} done ({chars} chars) → {file}',
365
- 'progress.reviewed': 'Chapter {no} review: {score} — {verdict}',
366
- 'progress.error': 'Chapter {no} failed: {message}',
367
- 'progress.rewriting': 'Revising chapter {no}…',
368
- 'progress.polishing': 'Polishing chapter {no}…',
369
- 'progress.empty': 'Generation/review progress appears here',
370
- 'assistant.hint': 'Discuss plot, characters, foreshadowing with the AI editor; once agreed, let it edit the outline, bible, or chapters directly.',
371
- 'assistant.placeholder': 'e.g. Add a hook at the end of chapter 2…',
372
- 'assistant.send': 'Send',
373
- 'assistant.toolStart': '⚙ Running {name}…',
374
- 'assistant.toolDone': '✓ {name} done: {detail}',
375
- 'assistant.toolError': '✗ {name} failed: {detail}',
376
- 'assistant.empty': 'No conversation yet. Chat with the AI editor.',
377
- 'status.projectNone': 'No project in the output directory yet. Load an outline first.',
378
- 'status.files': 'Generated files',
379
- 'api.error': 'Request failed',
380
- }
381
-
382
- /** Keys type for the zh dictionary. */
158
+ /** 面板文案键位(= zh 词典的键)。 */
383
159
  export type NovelKey = keyof typeof zh
@@ -10,9 +10,9 @@ import { NovelPanel } from './panel/NovelPanel.tsx'
10
10
  import css from './panel/panel.module.css'
11
11
 
12
12
  /** The injected panel container. */
13
- export const PANEL_VIEW_SELECTOR = '[data-dsh-novelforge-view]'
13
+ const PANEL_VIEW_SELECTOR = '[data-dsh-novelforge-view]'
14
14
 
15
- const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"]'
15
+ const CONVERSATION_COLUMN_SELECTOR = '[data-pane="conversation"], [class*="centerCol"]'
16
16
  const ACTIVE_ATTR = 'data-dsh-novelforge-active'
17
17
  /** Sibling panels' activation attributes (evicted when this panel opens). */
18
18
  const OTHER_ACTIVE_ATTRS = ['data-dsh-taskboard-active', 'data-dsh-ssh-active']
@@ -44,7 +44,8 @@ export function mountPanel(controller: PanelController, api: NovelApi): () => vo
44
44
  const column = conversationColumn()
45
45
  if (column === undefined) return
46
46
  container = document.createElement('div')
47
- container.dataset.dshNovelforgeView = 'true'
47
+ // 归属标记只在这里落地;取值与 PANEL_VIEW_SELECTOR 同源,字符串不写第二遍。
48
+ container.setAttribute(PANEL_VIEW_SELECTOR.slice(1, -1), 'true')
48
49
  container.className = css.view
49
50
  column.appendChild(container)
50
51
  root = createRoot(container)