@waterwx/dsh-novel-forge 2.0.0-alpha.1 → 2.0.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 +286 -285
- package/lib/client.js +19561 -19439
- package/lib/index.js +8159 -4456
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +0 -6
- package/lib/types/assets.d.ts +20 -9
- package/lib/types/assistant.d.ts +29 -0
- package/lib/types/author-assets.d.ts +1 -3
- package/lib/types/bookshelf.d.ts +13 -3
- package/lib/types/client/api.d.ts +136 -27
- package/lib/types/client/chip-prefs.d.ts +36 -0
- package/lib/types/client/docx.d.ts +2 -2
- package/lib/types/client/llmLive.d.ts +31 -0
- package/lib/types/client/locales.d.ts +15 -70
- package/lib/types/client/mount.d.ts +0 -2
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorHome.d.ts +13 -11
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +5 -1
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/ModelManager.d.ts +2 -2
- package/lib/types/client/panel/NovelPanel.d.ts +6 -1
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/SettingsView.d.ts +12 -9
- package/lib/types/client/panel/ShelfView.d.ts +10 -1
- package/lib/types/client/panel/SubPage.d.ts +27 -0
- package/lib/types/client/panel/TensionView.d.ts +18 -0
- package/lib/types/client/panel/TimelineView.d.ts +18 -0
- package/lib/types/client/panel/helpers.d.ts +5 -3
- package/lib/types/client/panel/market-radar-state.d.ts +56 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/views.d.ts +21 -35
- package/lib/types/client/run-status.d.ts +21 -0
- package/lib/types/client/sidebar-entry.d.ts +0 -2
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +78 -103
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/home.d.ts +11 -0
- package/lib/types/index.d.ts +26 -12
- package/lib/types/llm-live.d.ts +70 -0
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +18 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/model-capability.d.ts +93 -0
- package/lib/types/output-budget.d.ts +86 -0
- package/lib/types/prompt-slots.d.ts +57 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +816 -601
- package/lib/types/revision.d.ts +76 -0
- package/lib/types/routes.d.ts +9 -3
- package/lib/types/run.d.ts +23 -1
- package/lib/types/settings-persist.d.ts +59 -0
- package/lib/types/snapshots.d.ts +46 -0
- package/lib/types/stage-contract.d.ts +58 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +50 -25
- package/src/action-guard.ts +127 -0
- package/src/ai-scan.ts +0 -24
- package/src/assets.ts +1801 -879
- package/src/assistant.ts +1098 -770
- package/src/author-assets.ts +131 -147
- package/src/bookshelf.ts +193 -167
- package/src/client/api.ts +767 -617
- package/src/client/chip-prefs.ts +121 -0
- package/src/client/docx.ts +32 -69
- package/src/client/index.ts +2 -0
- package/src/client/llmLive.ts +111 -0
- package/src/client/locales.ts +15 -274
- package/src/client/mount.tsx +3 -2
- package/src/client/panel/AdaptModeView.tsx +535 -520
- package/src/client/panel/AiTag.tsx +7 -0
- package/src/client/panel/AssetsTab.tsx +448 -458
- package/src/client/panel/AssistantTab.tsx +355 -301
- package/src/client/panel/AuthorAssetsView.tsx +205 -207
- package/src/client/panel/AuthorHome.tsx +309 -204
- package/src/client/panel/BookAnalysisView.tsx +63 -0
- package/src/client/panel/CmdPalette.tsx +102 -0
- package/src/client/panel/CreateBookView.tsx +366 -352
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/GlobalAssetLibraryView.tsx +168 -172
- package/src/client/panel/IdeaInspirationView.tsx +76 -0
- package/src/client/panel/ImportModal.tsx +302 -302
- package/src/client/panel/KnowledgeBaseView.tsx +125 -0
- package/src/client/panel/LiveFeedLog.tsx +204 -0
- package/src/client/panel/MarketRadarView.tsx +415 -0
- package/src/client/panel/ModelManager.tsx +205 -205
- package/src/client/panel/NovelPanel.tsx +3144 -2205
- package/src/client/panel/PageHeader.tsx +37 -0
- package/src/client/panel/ProgressConsole.tsx +63 -64
- package/src/client/panel/PromptSlotsView.tsx +97 -0
- package/src/client/panel/ReaderView.tsx +306 -306
- package/src/client/panel/RunPanel.tsx +56 -24
- package/src/client/panel/SettingsView.tsx +175 -106
- package/src/client/panel/ShelfView.tsx +63 -39
- package/src/client/panel/SubPage.tsx +42 -0
- package/src/client/panel/TensionView.tsx +212 -0
- package/src/client/panel/TimelineView.tsx +208 -0
- package/src/client/panel/WorldTab.tsx +202 -202
- package/src/client/panel/helpers.ts +7 -5
- package/src/client/panel/market-radar-state.ts +79 -0
- package/src/client/panel/panel.module.css +5767 -4360
- package/src/client/panel/toast.ts +51 -0
- package/src/client/panel/views.tsx +316 -314
- package/src/client/run-status.ts +171 -0
- package/src/client/sidebar-entry.ts +3 -3
- package/src/docx-core.ts +53 -0
- package/src/docx.ts +44 -83
- package/src/engine.ts +1197 -1638
- package/src/global-assets.ts +58 -0
- package/src/home.ts +33 -0
- package/src/index.ts +181 -101
- package/src/llm-live.ts +245 -0
- package/src/llm-retry.ts +77 -0
- package/src/market-radar-scan.ts +35 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/migrate-output.ts +76 -0
- package/src/model-capability.ts +215 -0
- package/src/novel-context.ts +286 -287
- package/src/output-budget.ts +147 -0
- package/src/prompt-slots.ts +125 -0
- package/src/prompting.ts +26 -0
- package/src/protocol.ts +835 -614
- package/src/revision.ts +283 -0
- package/src/routes.ts +1286 -910
- package/src/run.ts +113 -17
- package/src/settings-persist.ts +114 -0
- package/src/snapshots.ts +174 -0
- package/src/stage-contract.ts +186 -0
- package/src/tension.ts +251 -0
- package/src/timeline.ts +159 -0
- package/assets/styles/arcane-thick-3d.webp +0 -0
- package/assets/styles/chinese-ink.webp +0 -0
- package/assets/styles/clay-stopmotion-3d.webp +0 -0
- package/assets/styles/dark-ghibli.webp +0 -0
- package/assets/styles/dark-gothic-3d.webp +0 -0
- package/assets/styles/film-photo.webp +0 -0
- package/assets/styles/guofeng-heavy.webp +0 -0
- package/assets/styles/guoman-2d-gufeng.webp +0 -0
- package/assets/styles/guoman-3d-tianyuan.webp +0 -0
- package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
- package/assets/styles/horror-game-3d.webp +0 -0
- package/assets/styles/hyperreal-3d.webp +0 -0
- package/assets/styles/japan-cel-dark.webp +0 -0
- package/assets/styles/korean-webtoon.webp +0 -0
- package/assets/styles/low-cyberpunk-3d.webp +0 -0
- package/assets/styles/nextgen-cg.webp +0 -0
- package/assets/styles/noir-film.webp +0 -0
- package/assets/styles/openworld-survival.webp +0 -0
- package/assets/styles/paper-cut-3d.webp +0 -0
- package/assets/styles/pixar-adult-3d.webp +0 -0
- package/assets/styles/pop-marvel-2d.webp +0 -0
- package/assets/styles/sandiao-qban.webp +0 -0
- package/assets/styles/shinkai-anime.webp +0 -0
- package/assets/styles/tactical-stealth.webp +0 -0
- package/assets/styles/urban-lightreal-3d.webp +0 -0
- package/assets/styles/vaporwave-3d.webp +0 -0
- package/assets/styles/vhs90.webp +0 -0
- package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
- package/lib/types/client/panel/FlowGuide.d.ts +0 -22
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
- package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
- package/lib/types/client/panel/ModelPicker.d.ts +0 -11
- package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
- package/lib/types/client/panel/PropLibrary.d.ts +0 -8
- package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
- package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
- package/lib/types/client/panel/StyleCard.d.ts +0 -8
- package/lib/types/client/panel/StyleGallery.d.ts +0 -1
- package/lib/types/comic-presets.d.ts +0 -12
- package/lib/types/manga-genre-rules.d.ts +0 -22
- package/lib/types/prompt-utils.d.ts +0 -35
- package/lib/types/review-policy.d.ts +0 -97
- package/lib/types/shot-language.d.ts +0 -46
- package/lib/types/story-beat-language.d.ts +0 -35
- package/lib/types/style-library.d.ts +0 -33
- package/skills/chapter-batch/SKILL.md +0 -81
- package/src/client/mount.tsx._new +0 -97
- package/src/client/mount.tsx._nf +0 -97
- package/src/client/mount.tsx._tmp +0 -97
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/ProgressHomeView.tsx +0 -87
- package/src/client/panel/PropLibrary.tsx +0 -175
- package/src/client/panel/SceneLibrary.tsx +0 -453
- package/src/client/panel/StoryboardTab.tsx +0 -472
- package/src/client/panel/StyleCard.tsx +0 -90
- package/src/client/panel/StyleGallery.tsx +0 -58
- package/src/comic-presets.ts +0 -67
- package/src/manga-genre-rules.ts +0 -87
- package/src/prompt-utils.ts +0 -73
- package/src/review-policy.ts +0 -161
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 全局资源库(跨书可复用的自定义题材/推进模式)。
|
|
3
|
+
* 雷达「同步底座到全局资源库」写入此处;/assets 返回时与内置库合并展示。
|
|
4
|
+
*/
|
|
5
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import type { GenreNode, ProgressionMode } from './protocol.ts'
|
|
7
|
+
import { dshHome, dshHomePath } from './home.ts'
|
|
8
|
+
|
|
9
|
+
interface GlobalAssets {
|
|
10
|
+
genres: GenreNode[]
|
|
11
|
+
modes: ProgressionMode[]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const FILE = dshHomePath('novel-forge-global-assets.json')
|
|
15
|
+
let cache: GlobalAssets | null = null
|
|
16
|
+
|
|
17
|
+
function load(): GlobalAssets {
|
|
18
|
+
if (cache !== null) return cache
|
|
19
|
+
try {
|
|
20
|
+
const raw = readFileSync(FILE, 'utf8')
|
|
21
|
+
const parsed = JSON.parse(raw) as Partial<GlobalAssets>
|
|
22
|
+
cache = { genres: Array.isArray(parsed.genres) ? parsed.genres : [], modes: Array.isArray(parsed.modes) ? parsed.modes : [] }
|
|
23
|
+
} catch {
|
|
24
|
+
cache = { genres: [], modes: [] }
|
|
25
|
+
}
|
|
26
|
+
return cache
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function persist(): void {
|
|
30
|
+
if (cache === null) return
|
|
31
|
+
mkdirSync(dshHome(), { recursive: true })
|
|
32
|
+
writeFileSync(FILE, JSON.stringify(cache, null, 2), 'utf8')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function globalGenreLibrary(): GenreNode[] {
|
|
36
|
+
return load().genres
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function globalProgressionLibrary(): ProgressionMode[] {
|
|
40
|
+
return load().modes
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 新增/已有则跳过;返回 true=新增。 */
|
|
44
|
+
export function addGlobalGenre(g: GenreNode): boolean {
|
|
45
|
+
const lib = load()
|
|
46
|
+
if (lib.genres.some(x => x.name === g.name)) return false
|
|
47
|
+
lib.genres.push({ ...g, children: g.children ?? [] })
|
|
48
|
+
persist()
|
|
49
|
+
return true
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function addGlobalMode(m: ProgressionMode): boolean {
|
|
53
|
+
const lib = load()
|
|
54
|
+
if (lib.modes.some(x => x.name === m.name)) return false
|
|
55
|
+
lib.modes.push({ ...m, payoffs: m.payoffs ?? [], risks: m.risks ?? [], primary: m.primary ?? false })
|
|
56
|
+
persist()
|
|
57
|
+
return true
|
|
58
|
+
}
|
package/src/home.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness home resolution for this plugin.
|
|
3
|
+
*
|
|
4
|
+
* Precedence mirrors `resolveDshHome` from `@deepseek-ai/dsh-home-paths`:
|
|
5
|
+
* `$DSH_HOME` when set and non-blank, else `~/.dsh`. That package's remaining
|
|
6
|
+
* input — the harness's own explicit override — is not reachable from a
|
|
7
|
+
* plugin, so this reproduces the part a plugin can observe instead of taking
|
|
8
|
+
* a dependency whose published range tracks a different harness line.
|
|
9
|
+
*
|
|
10
|
+
* State files must resolve through here rather than `~/.dsh` directly: the
|
|
11
|
+
* desktop build points `DSH_HOME` at its own root, and a hardcoded path made
|
|
12
|
+
* both builds share one store and lose it whenever that directory was cleaned.
|
|
13
|
+
*/
|
|
14
|
+
import { homedir } from 'node:os'
|
|
15
|
+
import { join, resolve } from 'node:path'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Absolute Harness home for this process.
|
|
19
|
+
* @returns the normalized home directory.
|
|
20
|
+
*/
|
|
21
|
+
export function dshHome(): string {
|
|
22
|
+
const configured = process.env.DSH_HOME?.trim()
|
|
23
|
+
return resolve(configured !== undefined && configured.length > 0 ? configured : join(homedir(), '.dsh'))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Join path segments onto the Harness home.
|
|
28
|
+
* @param segments - path segments appended to the home.
|
|
29
|
+
* @returns the normalized absolute joined path.
|
|
30
|
+
*/
|
|
31
|
+
export function dshHomePath(...segments: string[]): string {
|
|
32
|
+
return join(dshHome(), ...segments)
|
|
33
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -7,17 +7,18 @@
|
|
|
7
7
|
* changes.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { homedir } from 'node:os'
|
|
11
|
-
import { join } from 'node:path'
|
|
12
10
|
import type { Context } from '@deepseek-ai/cordis'
|
|
13
11
|
import type {} from '@deepseek-ai/dsh-settings'
|
|
14
12
|
import z from 'schemastery'
|
|
15
13
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
16
14
|
import type {} from '@deepseek-ai/dsh-llm'
|
|
17
15
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
18
|
-
import type { ConfigPatch,
|
|
16
|
+
import type { ConfigPatch, NovelConfig, ConfigResponse } from './protocol.ts'
|
|
19
17
|
import { makeRoutes } from './routes.ts'
|
|
20
18
|
import { activeBookOutputDir } from './bookshelf.ts'
|
|
19
|
+
import { loadProject } from './engine.ts'
|
|
20
|
+
import { dshHomePath } from './home.ts'
|
|
21
|
+
import { novelSettingsNamespace, persistConfigPatch, type SettingsLike } from './settings-persist.ts'
|
|
21
22
|
|
|
22
23
|
/** Stable cordis plugin name. */
|
|
23
24
|
export const name = 'novel-forge'
|
|
@@ -27,11 +28,18 @@ export const inject = ['webServer', 'llm', 'systemPrompt', 'settings']
|
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Settings namespace of the novel-forge capability — the section the web
|
|
30
|
-
* settings surface edits.
|
|
31
|
-
*
|
|
31
|
+
* settings surface edits.
|
|
32
|
+
*
|
|
33
|
+
* Legacy fallback only: from Harness 0.2 the Loader derives the section from this
|
|
34
|
+
* plugin's exported `Config`, and addresses it by the **entry id** it assigned in
|
|
35
|
+
* the active profile. That id is not guaranteed to be this name (bundle rows may
|
|
36
|
+
* be given a generated id), so the live id is read from the context instead —
|
|
37
|
+
* see {@link novelSettingsNamespace}. The constant stays for the browser half and
|
|
38
|
+
* for installations whose entry id really is this spelling.
|
|
32
39
|
*/
|
|
33
40
|
export const NOVEL_SETTINGS_NAMESPACE = 'dsh-novel-forge' as const
|
|
34
41
|
|
|
42
|
+
|
|
35
43
|
/** Plugin config, validated by the same-named schemastery schema. */
|
|
36
44
|
export interface Config {
|
|
37
45
|
/** When true (default), a system-prompt section announces the plugin to every agent. */
|
|
@@ -46,6 +54,16 @@ export interface Config {
|
|
|
46
54
|
provider?: string
|
|
47
55
|
/** LLM model id. */
|
|
48
56
|
model?: string
|
|
57
|
+
/** 任务级模型路由:正文生成模型(留空则跟随 model)。 */
|
|
58
|
+
generateModel?: string
|
|
59
|
+
/** 任务级模型路由:审稿模型(留空则跟随 model)。 */
|
|
60
|
+
reviewModel?: string
|
|
61
|
+
/** 任务级模型路由:质检模型(留空则跟随 model)。 */
|
|
62
|
+
auditModel?: string
|
|
63
|
+
/** 备用模型:主模型失败时自动切换重试一次(留空 = 不切换)。 */
|
|
64
|
+
fallbackModel?: string
|
|
65
|
+
/** 出章后自动抽取故事时间线(默认开)。 */
|
|
66
|
+
autoTimeline?: boolean
|
|
49
67
|
/** LLM reasoning effort (off/low/high/max). */
|
|
50
68
|
reasoningEffort?: 'off' | 'low' | 'high' | 'max'
|
|
51
69
|
/** 分析类任务(提炼/拆书/反推大纲等)的推理档位;默认 low。 */
|
|
@@ -62,14 +80,6 @@ export interface Config {
|
|
|
62
80
|
autoAuthorReview?: boolean
|
|
63
81
|
/** 修订/润色产出草稿后自动附带一次 AI 审查(默认开,可在设置页关闭省 token)。 */
|
|
64
82
|
autoReviewAfterRevise?: boolean
|
|
65
|
-
/** 生图模型库(多套并存,启用一条生效)。 */
|
|
66
|
-
imageModels?: ImageModelConfig[]
|
|
67
|
-
/** 豆包/Seedream 生图 API Key(旧字段,兼容迁移用)。 */
|
|
68
|
-
imageApiKey?: string
|
|
69
|
-
/** 豆包/Seedream 生图模型 ID(旧字段,兼容迁移用)。 */
|
|
70
|
-
imageApiModel?: string
|
|
71
|
-
/** 是否启用豆包生图(旧字段,兼容迁移用)。 */
|
|
72
|
-
imageApiEnabled?: boolean
|
|
73
83
|
/** 自定义背景图(URL / dataURL / 服务端路径引用)。 */
|
|
74
84
|
themeBackground?: string
|
|
75
85
|
/** 自定义背景遮罩/模糊强度 0-80。 */
|
|
@@ -80,46 +90,76 @@ export interface Config {
|
|
|
80
90
|
enableAdaptMode?: boolean
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Mark a config field as live-editable.
|
|
95
|
+
*
|
|
96
|
+
* From Harness 0.2 the settings surface builds its editable form from this
|
|
97
|
+
* plugin's exported `Config` and only exposes fields whose nearest ancestor
|
|
98
|
+
* declares volatility (`SettingsForms.schema` + `volatileForm`). Without the
|
|
99
|
+
* marker the host refuses a write with `Plugin entry "…" has no volatile
|
|
100
|
+
* fields`, so the plugin's own 保存设置 route could never persist anything.
|
|
101
|
+
*
|
|
102
|
+
* The canonical form is schemastery's `.volatile()` builder, but that builder
|
|
103
|
+
* ships only in the Harness fork (`@deepseek-ai/schemastery`); the npm
|
|
104
|
+
* `schemastery` this package depends on exposes the same switch as metadata
|
|
105
|
+
* (`extra('volatile', true)`), which is what the fork's builder sets anyway.
|
|
106
|
+
* Prefer the builder when the loaded copy has it, fall back to the metadata so
|
|
107
|
+
* the schema is correct on either copy.
|
|
108
|
+
* @param schema - the field schema.
|
|
109
|
+
* @returns the same field marked live-editable.
|
|
110
|
+
*/
|
|
111
|
+
function live<T>(schema: T): T {
|
|
112
|
+
const candidate = schema as unknown as {
|
|
113
|
+
volatile?: () => unknown
|
|
114
|
+
extra?: (key: string, value: boolean) => unknown
|
|
115
|
+
}
|
|
116
|
+
if (typeof candidate.volatile === 'function') return candidate.volatile() as T
|
|
117
|
+
if (typeof candidate.extra === 'function') return candidate.extra('volatile', true) as T
|
|
118
|
+
return schema
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Plugin config schema.
|
|
123
|
+
*
|
|
124
|
+
* Every field is live-editable (see {@link live}): the plugin re-reads the
|
|
125
|
+
* settings section on every request, so a change must apply without remounting.
|
|
126
|
+
*/
|
|
83
127
|
export const Config: z<Config> = z.object({
|
|
84
|
-
announceToAgent: z.boolean().default(true),
|
|
85
|
-
enabled: z.boolean().default(true),
|
|
86
|
-
outlinePath: z.string().default(''),
|
|
87
|
-
outputDir: z.string().default(
|
|
88
|
-
provider: z.string().default('deepseek-official'),
|
|
89
|
-
model: z.string().default('deepseek-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
imageApiModel: z.string().default(''),
|
|
108
|
-
imageApiEnabled: z.boolean().default(false),
|
|
109
|
-
themeBackground: z.string().default(''),
|
|
110
|
-
themeBackgroundBlur: z.number().default(0),
|
|
111
|
-
themeOpacity: z.number().default(100),
|
|
112
|
-
enableAdaptMode: z.boolean().default(true),
|
|
128
|
+
announceToAgent: live(z.boolean().default(true)),
|
|
129
|
+
enabled: live(z.boolean().default(true)),
|
|
130
|
+
outlinePath: live(z.string().default('')),
|
|
131
|
+
outputDir: live(z.string().default(dshHomePath('novels'))),
|
|
132
|
+
provider: live(z.string().default('deepseek-official')),
|
|
133
|
+
model: live(z.string().default('deepseek-flash')),
|
|
134
|
+
generateModel: live(z.string().default('')),
|
|
135
|
+
reviewModel: live(z.string().default('')),
|
|
136
|
+
auditModel: live(z.string().default('')),
|
|
137
|
+
fallbackModel: live(z.string().default('')),
|
|
138
|
+
autoTimeline: live(z.boolean().default(true)),
|
|
139
|
+
reasoningEffort: live(z.union(['off', 'low', 'high', 'max']).default('off')),
|
|
140
|
+
analysisReasoning: live(z.union(['off', 'low', 'high', 'max']).default('low')),
|
|
141
|
+
chapterChars: live(z.number().default(3500)),
|
|
142
|
+
maxTokens: live(z.number().default(12000)),
|
|
143
|
+
reviewPassScore: live(z.number().default(70)),
|
|
144
|
+
autoReview: live(z.boolean().default(true)),
|
|
145
|
+
autoAuthorReview: live(z.boolean().default(true)),
|
|
146
|
+
autoReviewAfterRevise: live(z.boolean().default(true)),
|
|
147
|
+
themeBackground: live(z.string().default('')),
|
|
148
|
+
themeBackgroundBlur: live(z.number().default(0)),
|
|
149
|
+
themeOpacity: live(z.number().default(100)),
|
|
150
|
+
enableAdaptMode: live(z.boolean().default(true)),
|
|
113
151
|
})
|
|
114
152
|
|
|
115
153
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
116
154
|
const DEFAULT_ANNOUNCE = true
|
|
117
155
|
const DEFAULT_OUTLINE_PATH = ''
|
|
118
|
-
const DEFAULT_OUTPUT_DIR =
|
|
156
|
+
const DEFAULT_OUTPUT_DIR = dshHomePath('novels')
|
|
119
157
|
const DEFAULT_PROVIDER = 'deepseek-official'
|
|
120
|
-
const DEFAULT_MODEL = 'deepseek-
|
|
158
|
+
const DEFAULT_MODEL = 'deepseek-flash'
|
|
121
159
|
const DEFAULT_REASONING_EFFORT = 'off' as const
|
|
122
160
|
const DEFAULT_ANALYSIS_REASONING = 'low' as const
|
|
161
|
+
const DEFAULT_FALLBACK_MODEL = ''
|
|
162
|
+
const DEFAULT_AUTO_TIMELINE = true
|
|
123
163
|
const DEFAULT_CHAPTER_CHARS = 3500
|
|
124
164
|
const DEFAULT_MAX_TOKENS = 12000
|
|
125
165
|
const DEFAULT_REVIEW_PASS_SCORE = 70
|
|
@@ -131,7 +171,7 @@ const DEFAULT_AUTO_REVIEW_AFTER_REVISE = true
|
|
|
131
171
|
const SECTION_ORDER = 160
|
|
132
172
|
|
|
133
173
|
/** Model-facing announcement: plugin presence, capabilities, and limits. */
|
|
134
|
-
export const NOVEL_GUIDANCE = '本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx
|
|
174
|
+
export const NOVEL_GUIDANCE = '本机已安装 dsh-novel-forge 插件(AI 编译小说工作台):侧边栏「小说工坊」入口。能力:读取 docx 大纲、粘贴大纲文本或把 txt 全本拆章导入;用 LLM 提炼道藏(人设/世界观/金手指规则/写作红线)与大世界(境界/区域/势力);生成卷计划与结构化章节计划(本章目标/剧情要点/必达项/义务合约/章末钩子);逐章生成 3000-4000 字正文并保存为 Markdown(默认输出到插件设置里「输出目录」指定的位置,未配置时落到 Harness 数据目录下的 novels/书名);每章自动生成摘要与编年录事实、抽取故事时间线(故事内时间/地点/在场角色/事件)、自动 AI 审稿(人设/设定/红线/文笔/爽点/逻辑/反 AI/呈现/合规九维)与作者复盘;支持按审稿意见合并修订(审稿 high + 时间线矛盾 + 张力偏差合成一轮,每章最多两轮后转人工)、去 AI 味润色、章节历史版本回滚、暗线(伏笔)管理、剧情线与角色库、张力曲线、全书一致性质检与敏感词扫描、分范围导出与项目备份、批量连写(生产单,可暂停/续跑)。限制:所有 AI 操作消耗 LLM API 额度;输出目录与模型可在插件设置中修改;章节正文质量取决于大纲完整度;张力/时间线问题默认只是建议(写日志与待办),不会自动改正文。用户提到「小说 / 大纲 / 写小说 / 章节 / 审稿 / 润色」时即指本插件,请据此协作。'
|
|
135
175
|
|
|
136
176
|
/** Resolve a config-like value into the full runtime config. */
|
|
137
177
|
export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
|
@@ -140,6 +180,11 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
|
|
140
180
|
outputDir: value?.outputDir ?? DEFAULT_OUTPUT_DIR,
|
|
141
181
|
provider: value?.provider ?? DEFAULT_PROVIDER,
|
|
142
182
|
model: value?.model ?? DEFAULT_MODEL,
|
|
183
|
+
generateModel: value?.generateModel,
|
|
184
|
+
reviewModel: value?.reviewModel,
|
|
185
|
+
auditModel: value?.auditModel,
|
|
186
|
+
fallbackModel: value?.fallbackModel ?? DEFAULT_FALLBACK_MODEL,
|
|
187
|
+
autoTimeline: value?.autoTimeline ?? DEFAULT_AUTO_TIMELINE,
|
|
143
188
|
reasoningEffort: value?.reasoningEffort ?? DEFAULT_REASONING_EFFORT,
|
|
144
189
|
analysisReasoning: value?.analysisReasoning ?? DEFAULT_ANALYSIS_REASONING,
|
|
145
190
|
chapterChars: value?.chapterChars ?? DEFAULT_CHAPTER_CHARS,
|
|
@@ -148,28 +193,6 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
|
|
148
193
|
autoReview: value?.autoReview ?? DEFAULT_AUTO_REVIEW,
|
|
149
194
|
autoAuthorReview: value?.autoAuthorReview ?? DEFAULT_AUTO_AUTHOR_REVIEW,
|
|
150
195
|
autoReviewAfterRevise: value?.autoReviewAfterRevise ?? DEFAULT_AUTO_REVIEW_AFTER_REVISE,
|
|
151
|
-
// 生图模型库:只有「从未保存过 imageModels」时才用旧配置迁移一条(豆包默认地址);
|
|
152
|
-
// 已保存为空数组 = 用户主动删光,保持空,不再复活。
|
|
153
|
-
imageModels: value?.imageModels !== undefined
|
|
154
|
-
? value.imageModels
|
|
155
|
-
: (value?.imageApiKey !== undefined && value.imageApiKey !== ''
|
|
156
|
-
? [{ id: 'img-legacy', name: '豆包(旧配置)', baseURL: DEFAULT_IMAGE_BASE, apiKey: value.imageApiKey, model: value.imageApiModel ?? '', enabled: true }]
|
|
157
|
-
: []),
|
|
158
|
-
// 运行时生效值 = 启用条目(兼容旧代码读取 imageApiKey/imageApiModel/imageApiEnabled)。
|
|
159
|
-
imageBaseUrl: (() => {
|
|
160
|
-
const active = (value?.imageModels ?? []).find(m => m.enabled) ?? (value?.imageModels ?? [])[0]
|
|
161
|
-
return active?.baseURL !== undefined && active.baseURL !== '' ? active.baseURL : (value?.imageApiKey !== undefined && value.imageApiKey !== '' ? DEFAULT_IMAGE_BASE : undefined)
|
|
162
|
-
})(),
|
|
163
|
-
imageApiKey: (() => {
|
|
164
|
-
const active = (value?.imageModels ?? []).find(m => m.enabled) ?? (value?.imageModels ?? [])[0]
|
|
165
|
-
return active?.apiKey ?? value?.imageApiKey
|
|
166
|
-
})(),
|
|
167
|
-
imageApiModel: (() => {
|
|
168
|
-
const active = (value?.imageModels ?? []).find(m => m.enabled) ?? (value?.imageModels ?? [])[0]
|
|
169
|
-
return active?.model ?? value?.imageApiModel
|
|
170
|
-
})(),
|
|
171
|
-
// 生图启用:总开关打开,或模型库里存在启用条目,均视为已启用(避免用户只配模型忘了开总开关)。
|
|
172
|
-
imageApiEnabled: value?.imageApiEnabled === true || (value?.imageModels ?? []).some(m => m.enabled === true),
|
|
173
196
|
themeBackground: value?.themeBackground ?? '',
|
|
174
197
|
themeBackgroundBlur: value?.themeBackgroundBlur ?? 0,
|
|
175
198
|
themeOpacity: value?.themeOpacity ?? 100,
|
|
@@ -177,34 +200,70 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
|
|
177
200
|
}
|
|
178
201
|
}
|
|
179
202
|
|
|
180
|
-
/** 生图默认接口地址(豆包 ark,OpenAI 兼容)。 */
|
|
181
|
-
const DEFAULT_IMAGE_BASE = 'https://ark.cn-beijing.volces.com/api/v3'
|
|
182
|
-
|
|
183
203
|
/**
|
|
184
204
|
* Mount the routes and announcement.
|
|
185
205
|
* @param ctx - host plugin context carrying webServer/llm/systemPrompt.
|
|
186
206
|
* @param config - resolved plugin config (schema defaults applied by the loader).
|
|
187
207
|
*/
|
|
188
208
|
export function apply(ctx: Context, config?: Config): void {
|
|
189
|
-
// The live source the routes read
|
|
190
|
-
//
|
|
191
|
-
|
|
209
|
+
// The live source the routes read. Held as an explicit state object, not a
|
|
210
|
+
// self-referential closure: `current = () => ({ ...current(), ...patch })`
|
|
211
|
+
// re-reads `current` inside the function it is assigning, so the first save
|
|
212
|
+
// recurses until the stack blows (`Maximum call stack size exceeded`).
|
|
213
|
+
let currentState: Config = config ?? {}
|
|
214
|
+
const current = (): Config => currentState
|
|
215
|
+
const setCurrent = (next: Config): void => { currentState = next }
|
|
192
216
|
const resolve = (): NovelConfig => {
|
|
193
217
|
const resolved = resolveConfig(current())
|
|
194
218
|
// 书架激活的书优先决定输出目录(settings 仍可改默认值)。
|
|
195
219
|
const shelfDir = activeBookOutputDir()
|
|
196
|
-
if (shelfDir
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
220
|
+
if (shelfDir === undefined) return resolved
|
|
221
|
+
// 本书参数覆盖(novel-project.json.bookSettings;未设字段回退全局)。
|
|
222
|
+
const bs = loadProject(shelfDir)?.bookSettings
|
|
223
|
+
const book: NovelConfig = { ...resolved, outputDir: shelfDir }
|
|
224
|
+
if (bs === undefined) return book
|
|
225
|
+
if (bs.provider !== undefined) book.provider = bs.provider
|
|
226
|
+
if (bs.model !== undefined) book.model = bs.model
|
|
227
|
+
if (bs.reasoningEffort !== undefined) book.reasoningEffort = bs.reasoningEffort
|
|
228
|
+
if (bs.analysisReasoning !== undefined) book.analysisReasoning = bs.analysisReasoning
|
|
229
|
+
if (bs.chapterChars !== undefined) book.chapterChars = bs.chapterChars
|
|
230
|
+
if (bs.maxTokens !== undefined) book.maxTokens = bs.maxTokens
|
|
231
|
+
if (bs.reviewPassScore !== undefined) book.reviewPassScore = bs.reviewPassScore
|
|
232
|
+
if (bs.autoReview !== undefined) book.autoReview = bs.autoReview
|
|
233
|
+
if (bs.autoAuthorReview !== undefined) book.autoAuthorReview = bs.autoAuthorReview
|
|
234
|
+
if (bs.autoReviewAfterRevise !== undefined) book.autoReviewAfterRevise = bs.autoReviewAfterRevise
|
|
235
|
+
return book
|
|
200
236
|
}
|
|
201
237
|
|
|
202
|
-
|
|
238
|
+
/**
|
|
239
|
+
* The settings section this instance is addressed by, resolved once at mount.
|
|
240
|
+
* Undefined when the host exposes neither an entry id nor a registered section,
|
|
241
|
+
* in which case config edits stay in memory (see {@link patchConfig}).
|
|
242
|
+
*/
|
|
243
|
+
const settingsNs = novelSettingsNamespace(ctx)
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Persist one config patch.
|
|
247
|
+
*
|
|
248
|
+
* Applies to the live config either way. Persistence goes through the host
|
|
249
|
+
* settings section when that section is addressable; when it is not (unknown
|
|
250
|
+
* entry id, or the section never registered), the patch is kept in memory and
|
|
251
|
+
* the caller is told it will not survive a restart — a config save must never
|
|
252
|
+
* fail the request just because the host does not offer persistence.
|
|
253
|
+
* @param patch - the subset to merge.
|
|
254
|
+
* @returns the config the host will read next, plus a persistence caveat.
|
|
255
|
+
*/
|
|
256
|
+
const patchConfig = async (patch: ConfigPatch): Promise<ConfigResponse> => {
|
|
203
257
|
const next: ConfigPatch = {}
|
|
204
258
|
if (patch.outlinePath !== undefined) next.outlinePath = patch.outlinePath
|
|
205
259
|
if (patch.outputDir !== undefined) next.outputDir = patch.outputDir
|
|
206
260
|
if (patch.provider !== undefined) next.provider = patch.provider
|
|
207
261
|
if (patch.model !== undefined) next.model = patch.model
|
|
262
|
+
if (patch.generateModel !== undefined) next.generateModel = patch.generateModel
|
|
263
|
+
if (patch.reviewModel !== undefined) next.reviewModel = patch.reviewModel
|
|
264
|
+
if (patch.auditModel !== undefined) next.auditModel = patch.auditModel
|
|
265
|
+
if (patch.fallbackModel !== undefined) next.fallbackModel = patch.fallbackModel
|
|
266
|
+
if (patch.autoTimeline !== undefined) next.autoTimeline = patch.autoTimeline
|
|
208
267
|
if (patch.reasoningEffort !== undefined) next.reasoningEffort = patch.reasoningEffort
|
|
209
268
|
if (patch.analysisReasoning !== undefined) next.analysisReasoning = patch.analysisReasoning
|
|
210
269
|
if (patch.chapterChars !== undefined) next.chapterChars = patch.chapterChars
|
|
@@ -213,24 +272,28 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
213
272
|
if (patch.autoReview !== undefined) next.autoReview = patch.autoReview
|
|
214
273
|
if (patch.autoAuthorReview !== undefined) next.autoAuthorReview = patch.autoAuthorReview
|
|
215
274
|
if (patch.autoReviewAfterRevise !== undefined) next.autoReviewAfterRevise = patch.autoReviewAfterRevise
|
|
216
|
-
if (patch.imageApiKey !== undefined) next.imageApiKey = patch.imageApiKey
|
|
217
|
-
if (patch.imageApiModel !== undefined) next.imageApiModel = patch.imageApiModel
|
|
218
|
-
if (patch.imageApiEnabled !== undefined) next.imageApiEnabled = patch.imageApiEnabled
|
|
219
275
|
if (patch.themeBackground !== undefined) next.themeBackground = patch.themeBackground
|
|
220
276
|
if (patch.themeBackgroundBlur !== undefined) next.themeBackgroundBlur = patch.themeBackgroundBlur
|
|
221
277
|
if (patch.themeOpacity !== undefined) next.themeOpacity = patch.themeOpacity
|
|
222
278
|
if (patch.enableAdaptMode !== undefined) next.enableAdaptMode = patch.enableAdaptMode
|
|
223
|
-
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
279
|
+
// Apply + persist via the shared helper: the host section is addressed by the
|
|
280
|
+
// entry id the Loader assigned, never by a guessed constant. (ctx.get is the
|
|
281
|
+
// non-strict service access — same pattern the 0.1.x wiring used.)
|
|
282
|
+
const settings = ctx.get('settings') as SettingsLike | undefined
|
|
283
|
+
const persisted = await persistConfigPatch(
|
|
284
|
+
current() as unknown as Record<string, unknown>,
|
|
285
|
+
next as Record<string, unknown>,
|
|
286
|
+
settings,
|
|
287
|
+
settingsNs,
|
|
288
|
+
NOVEL_SETTINGS_NAMESPACE,
|
|
289
|
+
)
|
|
290
|
+
// Keep the live source in sync even when persistence went through the host:
|
|
291
|
+
// until the host reloads the plugin, the state object is still the old snapshot.
|
|
292
|
+
setCurrent(persisted.config as Config)
|
|
293
|
+
if (persisted.settingsWarning !== undefined) {
|
|
294
|
+
ctx.logger('novel-forge').warn('%s', persisted.settingsWarning)
|
|
232
295
|
}
|
|
233
|
-
return resolve()
|
|
296
|
+
return { config: resolve(), ...(persisted.settingsWarning === undefined ? {} : { settingsWarning: persisted.settingsWarning }) }
|
|
234
297
|
}
|
|
235
298
|
|
|
236
299
|
let disposeSection: (() => void) | undefined
|
|
@@ -254,7 +317,7 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
254
317
|
text: NOVEL_GUIDANCE,
|
|
255
318
|
})
|
|
256
319
|
}
|
|
257
|
-
const routes = makeRoutes({ ctx, getConfig: resolve, patchConfig })
|
|
320
|
+
const routes = makeRoutes({ ctx, getConfig: resolve, patchConfig, rawConfig: () => current(), settingsNs })
|
|
258
321
|
disposeRoutes = ctx.effect(
|
|
259
322
|
() => {
|
|
260
323
|
const disposers = routes.map(route => ctx.webServer.register(route))
|
|
@@ -265,20 +328,37 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
265
328
|
void value
|
|
266
329
|
}
|
|
267
330
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
331
|
+
// settings 接线做跨版本兼容(2026-09-30 适配 dsh 0.2.0-rc.2):
|
|
332
|
+
// 0.1.x 提供 installSection(注册配置段并回调 setSource/onChange),段 id 由
|
|
333
|
+
// 调用方指定,因此用声明的常量;
|
|
334
|
+
// 0.2.x 移除了它,改由 Loader 从本插件导出的 Config schema 推导配置段(段 id
|
|
335
|
+
// = 该 entry 的 id),settings 服务只剩 configure/describe/update/replace/mutate。
|
|
336
|
+
// 缺失时必须静默跳过,不能让整棵插件挂载失败(此前 novel-forge 因此整包不加载)。
|
|
337
|
+
const settingsService = ctx.get('settings') as unknown as
|
|
338
|
+
| { installSection?: (c: unknown, ns: string, schema: unknown, cfg: unknown, hooks: unknown) => void }
|
|
339
|
+
| undefined
|
|
340
|
+
if (settingsService !== undefined && typeof settingsService.installSection === 'function') {
|
|
341
|
+
settingsService.installSection(ctx, settingsNs ?? NOVEL_SETTINGS_NAMESPACE, Config, config ?? {}, {
|
|
342
|
+
setSource: (source: () => Config) => {
|
|
343
|
+
// Snapshot the host-provided source. The live source is one state object
|
|
344
|
+
// now, so a source that recomputed per call would have to be polled;
|
|
345
|
+
// 0.1.x wiring is legacy and the composition entry is authoritative here.
|
|
346
|
+
setCurrent(source())
|
|
347
|
+
sync()
|
|
348
|
+
},
|
|
349
|
+
onChange: sync,
|
|
350
|
+
})
|
|
351
|
+
}
|
|
275
352
|
|
|
276
353
|
// 运行时 skill:章节批量生产与值守处理(只在本插件环境可用)。
|
|
277
354
|
// 通过 ctx.get('skills') 获取注册表(服务存在时注册,缺失则跳过,不影响其他能力)。
|
|
278
|
-
const skillsService = ctx.get('skills') as { register?: (skill: { name: string; description: string; content: string }) => () => void } | undefined
|
|
279
|
-
|
|
355
|
+
const skillsService = ctx.get('skills') as { register?: (skill: { name: string; provider: string; description: string; content: string }) => () => void } | undefined
|
|
356
|
+
// [disabled 2026-09-05] 运行时 skill 注册按用户要求暂时关闭(排查 .length 报错)。
|
|
357
|
+
const enableRuntimeSkill: boolean = false
|
|
358
|
+
if (enableRuntimeSkill && skillsService !== undefined && skillsService.register !== undefined) {
|
|
280
359
|
const disposeSkill = skillsService.register({
|
|
281
360
|
name: 'novel-forge-chapter-batch',
|
|
361
|
+
provider: 'dsh-novel-forge',
|
|
282
362
|
description: '批量生成小说章节并值守处理审稿未过的章节(豁免/按意见修订/验证模式/重新生成),依赖 dsh-novel-forge 的 /api/dsh-novel-forge/* 路由。',
|
|
283
363
|
content: [
|
|
284
364
|
'# 小说章节批量生产与值守处理',
|