@waterwx/dsh-novel-forge 1.7.3 → 1.9.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.
Files changed (44) hide show
  1. package/lib/client.js +3627 -447
  2. package/lib/client.js.map +1 -1
  3. package/lib/index.js +1127 -141
  4. package/lib/index.js.map +1 -1
  5. package/lib/types/author-assets.d.ts +25 -0
  6. package/lib/types/client/api.d.ts +31 -1
  7. package/lib/types/client/locales.d.ts +16 -0
  8. package/lib/types/client/panel/AdaptModeView.d.ts +4 -0
  9. package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
  10. package/lib/types/client/panel/AuthorHome.d.ts +30 -0
  11. package/lib/types/client/panel/ModelManager.d.ts +14 -0
  12. package/lib/types/client/panel/ModelPicker.d.ts +11 -0
  13. package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
  14. package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
  15. package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
  16. package/lib/types/client/panel/SettingsView.d.ts +14 -0
  17. package/lib/types/client/text.d.ts +9 -0
  18. package/lib/types/engine.d.ts +39 -1
  19. package/lib/types/index.d.ts +8 -0
  20. package/lib/types/protocol.d.ts +339 -0
  21. package/package.json +1 -1
  22. package/src/author-assets.ts +166 -0
  23. package/src/client/api.ts +82 -0
  24. package/src/client/locales.ts +32 -0
  25. package/src/client/mount.tsx._new +97 -0
  26. package/src/client/mount.tsx._nf +97 -0
  27. package/src/client/mount.tsx._tmp +97 -0
  28. package/src/client/panel/AdaptModeView.tsx +202 -0
  29. package/src/client/panel/AuthorAssetsView.tsx +206 -0
  30. package/src/client/panel/AuthorHome.tsx +107 -0
  31. package/src/client/panel/ImportModal.tsx +2 -1
  32. package/src/client/panel/ModelManager.tsx +205 -0
  33. package/src/client/panel/NovelPanel.tsx +73 -79
  34. package/src/client/panel/ProgressConsole.tsx +64 -0
  35. package/src/client/panel/ProgressHomeView.tsx +87 -0
  36. package/src/client/panel/ReasoningSection.tsx +31 -0
  37. package/src/client/panel/SettingsView.tsx +292 -0
  38. package/src/client/panel/panel.module.css +325 -0
  39. package/src/client/sidebar-entry.ts +1 -1
  40. package/src/client/text.ts +28 -0
  41. package/src/engine.ts +442 -0
  42. package/src/index.ts +20 -0
  43. package/src/protocol.ts +356 -0
  44. package/src/routes.ts +323 -0
package/src/routes.ts CHANGED
@@ -13,6 +13,7 @@ import { spawn } from 'node:child_process'
13
13
  import { copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
14
14
  import { join, basename, extname } from 'node:path'
15
15
  import { homedir } from 'node:os'
16
+ import { randomBytes } from 'node:crypto'
16
17
  import { fileURLToPath } from 'node:url'
17
18
  import type { WebRoute } from '@deepseek-ai/dsh-host-webserver'
18
19
  import type { Context } from '@deepseek-ai/cordis'
@@ -21,6 +22,13 @@ import {
21
22
  NOVEL_API,
22
23
  type AssetsPatch,
23
24
  type AssetsResponse,
25
+ type AddModelRequest,
26
+ type AddModelResponse,
27
+ type LlmModelsResponse,
28
+ type LlmProvidersResponse,
29
+ type LlmVendorsResponse,
30
+ type RemoveProviderRequest,
31
+ type RemoveProviderResponse,
24
32
  type AssistantFrame,
25
33
  type AssistantHistoryResponse,
26
34
  type AssistantRequest,
@@ -100,15 +108,27 @@ import {
100
108
  type StoryboardPromptsRequest,
101
109
  type ImageTestRequest,
102
110
  type ImageTestResponse,
111
+ type LlmTestRequest,
112
+ type LlmTestResponse,
103
113
  type StoryboardPromptsResponse,
104
114
  type StyleEngineRequest,
105
115
  type SummaryRequest,
106
116
  type VolumesRequest,
107
117
  type VolumesResponse,
118
+ type AuthorAssetsResponse,
119
+ type AuthorAssetUpsertRequest,
120
+ type AuthorAssetRemoveRequest,
121
+ type AdaptAnalyzeRequest,
122
+ type AdaptAnalyzeResponse,
123
+ type AdaptProposeRequest,
124
+ type AdaptProposeResponse,
125
+ type AdaptExecuteRequest,
126
+ type AdaptExecuteResponse,
108
127
  } from './protocol.ts'
109
128
  import { readOutlineFromDocx } from './docx.ts'
110
129
  import { clearAssistantHistory, loadAssistantHistory, runAssistantTurn } from './assistant.ts'
111
130
  import { activateBook, bookshelfSnapshot, createBook, defaultOutputDirFor, importDir, loadBookshelf, removeBook, renameBook, seedBookshelfFromOutputDir } from './bookshelf.ts'
131
+ import { loadAuthorAssets, upsertAuthorAsset, removeAuthorAsset, importDefaultAuthorAssets } from './author-assets.ts'
112
132
  import { BUILTIN_ANTI_AI_RULES, BUILTIN_GENRE_LIBRARY, BUILTIN_PROGRESSION_MODES, BUILTIN_STYLE_TEMPLATES, emptyProjectAssets } from './assets.ts'
113
133
  import {
114
134
  chapterFileName,
@@ -153,9 +173,18 @@ import {
153
173
  suggestOutlines,
154
174
  reverseOutlineFromChapters,
155
175
  breakdownBook,
176
+ analyzeAdaptation,
177
+ proposeAdaptation,
178
+ applyAdaptationReplacements,
156
179
  generateRoleReferenceImage,
157
180
  generateMangaRoleReferenceImage,
158
181
  testImageEndpoint,
182
+ testLlmModel,
183
+ registerLlmModel,
184
+ listLlmVendors,
185
+ listLlmModels,
186
+ listLlmProviders,
187
+ removeLlmProvider,
159
188
  generateStoryboardSkeleton,
160
189
  generateStoryboardTable,
161
190
  generateStoryboardPrompts,
@@ -2424,6 +2453,117 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2424
2453
  },
2425
2454
  }
2426
2455
 
2456
+ // ------------------------------------------------------------- llm-test
2457
+ /** LLM 连通性测试:对选中的提供商/模型发一次最小真实调用。 */
2458
+ const llmTestRoute: WebRoute = {
2459
+ kind: 'exact',
2460
+ path: NOVEL_API.llmTest,
2461
+ handler: async (req, res) => {
2462
+ if (!guard(req, res, 'POST')) return
2463
+ const body = await readJsonBody<LlmTestRequest>(req)
2464
+ const provider = (body?.provider ?? '').trim()
2465
+ const model = (body?.model ?? '').trim()
2466
+ if (provider === '' || model === '') {
2467
+ writeJson(res, 400, { error: 'provider 与 model 必填' })
2468
+ return
2469
+ }
2470
+ const result = await testLlmModel(ctx, provider, model)
2471
+ writeJson(res, 200, result satisfies LlmTestResponse)
2472
+ },
2473
+ }
2474
+
2475
+ // ------------------------------------------------------------- llm-add
2476
+ /** 添加模型:厂商直填 API Key,或自定义 OpenAI 兼容路由(写 DSH 凭据 + router)。 */
2477
+ const addModelRoute: WebRoute = {
2478
+ kind: 'exact',
2479
+ path: NOVEL_API.addModel,
2480
+ handler: async (req, res) => {
2481
+ if (!guard(req, res, 'POST')) return
2482
+ const body = await readJsonBody<AddModelRequest>(req)
2483
+ if (body === undefined) {
2484
+ writeJson(res, 400, { error: '缺少请求体' })
2485
+ return
2486
+ }
2487
+ try {
2488
+ const result = await registerLlmModel(ctx, body)
2489
+ writeJson(res, 200, result satisfies AddModelResponse)
2490
+ } catch (error) {
2491
+ writeJson(res, 500, { error: `添加模型失败:${(error as Error).message}` })
2492
+ }
2493
+ },
2494
+ }
2495
+
2496
+ // ---------------------------------------------------------- llm-vendors
2497
+ /** 运行时厂商目录(DSH pi-ai 可配置提供方 + 内置适配器)。 */
2498
+ const llmVendorsRoute: WebRoute = {
2499
+ kind: 'exact',
2500
+ path: NOVEL_API.llmVendors,
2501
+ handler: async (req, res) => {
2502
+ if (!guard(req, res, 'GET')) return
2503
+ try {
2504
+ const result = await listLlmVendors(ctx)
2505
+ writeJson(res, 200, result satisfies LlmVendorsResponse)
2506
+ } catch (error) {
2507
+ writeJson(res, 500, { error: `读取厂商目录失败:${(error as Error).message}` })
2508
+ }
2509
+ },
2510
+ }
2511
+
2512
+ // ---------------------------------------------------------- llm-models
2513
+ /** 查询某个 provider 当前可用的模型(添加成功后可即时刷新下拉)。 */
2514
+ const llmModelsRoute: WebRoute = {
2515
+ kind: 'exact',
2516
+ path: NOVEL_API.llmModels,
2517
+ handler: async (req, res) => {
2518
+ if (!guard(req, res, 'GET')) return
2519
+ const url = new URL(req.url ?? '', 'http://localhost')
2520
+ const provider = decodeURIComponent((url.searchParams.get('provider') ?? '').trim())
2521
+ try {
2522
+ const result = await listLlmModels(ctx, provider)
2523
+ writeJson(res, 200, result satisfies LlmModelsResponse)
2524
+ } catch (error) {
2525
+ writeJson(res, 500, { error: `读取模型列表失败:${(error as Error).message}` })
2526
+ }
2527
+ },
2528
+ }
2529
+
2530
+ // ---------------------------------------------------------- llm-providers
2531
+ /** 已注册的提供方路由列表(提供方管理卡片)。 */
2532
+ const llmProvidersRoute: WebRoute = {
2533
+ kind: 'exact',
2534
+ path: NOVEL_API.llmProviders,
2535
+ handler: async (req, res) => {
2536
+ if (!guard(req, res, 'GET')) return
2537
+ try {
2538
+ const result = await listLlmProviders(ctx)
2539
+ writeJson(res, 200, result satisfies LlmProvidersResponse)
2540
+ } catch (error) {
2541
+ writeJson(res, 500, { error: `读取提供方列表失败:${(error as Error).message}` })
2542
+ }
2543
+ },
2544
+ }
2545
+
2546
+ // ------------------------------------------------------------- llm-remove
2547
+ /** 移除一个提供方(unset key + 移除 llm-pi-ai 路由)。 */
2548
+ const llmRemoveRoute: WebRoute = {
2549
+ kind: 'exact',
2550
+ path: NOVEL_API.llmRemove,
2551
+ handler: async (req, res) => {
2552
+ if (!guard(req, res, 'POST')) return
2553
+ const body = await readJsonBody<RemoveProviderRequest>(req)
2554
+ if (body === undefined) {
2555
+ writeJson(res, 400, { error: '缺少请求体' })
2556
+ return
2557
+ }
2558
+ try {
2559
+ const result = await removeLlmProvider(ctx, body)
2560
+ writeJson(res, 200, result satisfies RemoveProviderResponse)
2561
+ } catch (error) {
2562
+ writeJson(res, 500, { error: `移除提供方失败:${(error as Error).message}` })
2563
+ }
2564
+ },
2565
+ }
2566
+
2427
2567
  // ---------------------------------------------------- storyboard table
2428
2568
  /** 分镜·导演级:骨架 → 分镜表(镜头级)。 */
2429
2569
  const storyboardTableRoute: WebRoute = {
@@ -2754,6 +2894,174 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2754
2894
  },
2755
2895
  }
2756
2896
 
2897
+ // -------------------------------------------------- author assets (总数据)
2898
+ const authorAssetsRoute: WebRoute = {
2899
+ kind: 'exact',
2900
+ path: NOVEL_API.authorAssets,
2901
+ handler: (req, res) => {
2902
+ if (!guard(req, res, 'GET')) return
2903
+ const response: AuthorAssetsResponse = { assets: loadAuthorAssets() }
2904
+ writeJson(res, 200, response)
2905
+ },
2906
+ }
2907
+
2908
+ const authorAssetsUpsertRoute: WebRoute = {
2909
+ kind: 'exact',
2910
+ path: NOVEL_API.authorAssetsUpsert,
2911
+ handler: async (req, res) => {
2912
+ if (!guard(req, res, 'POST')) return
2913
+ const body = await readJsonBody<AuthorAssetUpsertRequest>(req)
2914
+ const asset = body?.asset
2915
+ if (asset === undefined || asset.name.trim() === '' || asset.content.trim() === '') {
2916
+ writeJson(res, 400, { error: 'asset 须含 name / content' })
2917
+ return
2918
+ }
2919
+ const response: AuthorAssetsResponse = { assets: upsertAuthorAsset(asset) }
2920
+ writeJson(res, 200, response)
2921
+ },
2922
+ }
2923
+
2924
+ const authorAssetsRemoveRoute: WebRoute = {
2925
+ kind: 'exact',
2926
+ path: NOVEL_API.authorAssetsRemove,
2927
+ handler: async (req, res) => {
2928
+ if (!guard(req, res, 'POST')) return
2929
+ const body = await readJsonBody<AuthorAssetRemoveRequest>(req)
2930
+ const id = body?.id ?? ''
2931
+ if (id === '') { writeJson(res, 400, { error: 'id 必填' }); return }
2932
+ const response: AuthorAssetsResponse = { assets: removeAuthorAsset(id) }
2933
+ writeJson(res, 200, response)
2934
+ },
2935
+ }
2936
+
2937
+ const authorAssetsImportDefaultRoute: WebRoute = {
2938
+ kind: 'exact',
2939
+ path: NOVEL_API.authorAssetsImportDefault,
2940
+ handler: (req, res) => {
2941
+ if (!guard(req, res, 'POST')) return
2942
+ const response: AuthorAssetsResponse = { assets: importDefaultAuthorAssets() }
2943
+ writeJson(res, 200, response)
2944
+ },
2945
+ }
2946
+
2947
+ // --------------------------------------------------- adaptation (改编 P0 分析)
2948
+ const adaptAnalyzeRoute: WebRoute = {
2949
+ kind: 'exact',
2950
+ path: NOVEL_API.adaptAnalyze,
2951
+ handler: async (req, res) => {
2952
+ if (!guard(req, res, 'POST')) return
2953
+ if (!getConfig().enableAdaptMode) { writeJson(res, 404, { error: '改编模式未开启' }); return }
2954
+ const body = await readJsonBody<AdaptAnalyzeRequest>(req)
2955
+ const config = getConfig()
2956
+ let text = body?.text?.trim() ?? ''
2957
+ if (text === '' && body?.filePath !== undefined && body.filePath !== '') {
2958
+ try {
2959
+ text = readFileSync(body.filePath, 'utf8')
2960
+ } catch (err) {
2961
+ writeJson(res, 400, { error: '读取文件失败:' + (err as Error).message })
2962
+ return
2963
+ }
2964
+ }
2965
+ if (text.length < 200) {
2966
+ writeJson(res, 400, { error: '全文内容过短(<200 字符),请上传完整小说文本' })
2967
+ return
2968
+ }
2969
+ try {
2970
+ const response: AdaptAnalyzeResponse = await analyzeAdaptation(ctx, config, text)
2971
+ writeJson(res, 200, response)
2972
+ } catch (err) {
2973
+ writeJson(res, 400, { error: (err as Error).message })
2974
+ }
2975
+ },
2976
+ }
2977
+
2978
+ const adaptProposeRoute: WebRoute = {
2979
+ kind: 'exact',
2980
+ path: NOVEL_API.adaptPropose,
2981
+ handler: async (req, res) => {
2982
+ if (!guard(req, res, 'POST')) return
2983
+ if (!getConfig().enableAdaptMode) { writeJson(res, 404, { error: '改编模式未开启' }); return }
2984
+ const body = await readJsonBody<AdaptProposeRequest>(req)
2985
+ const config = getConfig()
2986
+ const text = (body?.text ?? '').trim()
2987
+ const selections = body?.selections ?? []
2988
+ if (text.length < 200 || selections.length === 0) {
2989
+ writeJson(res, 400, { error: '请提供全文与至少一条要改的维度' })
2990
+ return
2991
+ }
2992
+ try {
2993
+ const response: AdaptProposeResponse = await proposeAdaptation(ctx, config, text, selections, body?.dimensions)
2994
+ writeJson(res, 200, response)
2995
+ } catch (err) {
2996
+ writeJson(res, 400, { error: (err as Error).message })
2997
+ }
2998
+ },
2999
+ }
3000
+
3001
+ const adaptExecuteRoute: WebRoute = {
3002
+ kind: 'exact',
3003
+ path: NOVEL_API.adaptExecute,
3004
+ handler: async (req, res) => {
3005
+ if (!guard(req, res, 'POST')) return
3006
+ if (!getConfig().enableAdaptMode) { writeJson(res, 404, { error: '改编模式未开启' }); return }
3007
+ const body = await readJsonBody<AdaptExecuteRequest>(req)
3008
+ const text = body?.text ?? ''
3009
+ const mappings = body?.mappings ?? []
3010
+ if (text.length < 200 || mappings.length === 0) {
3011
+ writeJson(res, 400, { error: '请提供全文与映射表' })
3012
+ return
3013
+ }
3014
+ if (body?.mode === 'rewrite') {
3015
+ writeJson(res, 400, { error: 'rewrite 模式尚未实现,请使用 replace(术语替换)' })
3016
+ return
3017
+ }
3018
+ const { adaptedText, hits } = applyAdaptationReplacements(text, mappings)
3019
+ const response: AdaptExecuteResponse = { adaptedText, mappings: mappings.length, hits }
3020
+ writeJson(res, 200, response)
3021
+ },
3022
+ }
3023
+
3024
+ // ------------------------------------------ theme custom background (upload + serve)
3025
+ const THEME_BG_DIR = join(homedir(), '.dsh', 'dsh-novel-forge-assets')
3026
+ const MIME_BY_EXT: Record<string, string> = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif' }
3027
+
3028
+ const themeBackgroundUploadRoute: WebRoute = {
3029
+ kind: 'exact',
3030
+ path: NOVEL_API.themeBackgroundUpload,
3031
+ handler: async (req, res) => {
3032
+ if (!guard(req, res, 'POST')) return
3033
+ const body = await readJsonBody<{ dataUrl?: string }>(req)
3034
+ const dataUrl = body?.dataUrl ?? ''
3035
+ const m = /^data:image\/(png|jpeg|jpg|webp|gif);base64,(.+)$/i.exec(dataUrl)
3036
+ if (m === null) { writeJson(res, 400, { error: '仅支持 PNG/JPG/WebP/GIF 图片' }); return }
3037
+ const ext = m[1]!.toLowerCase() === 'jpg' ? 'jpeg' : m[1]!.toLowerCase()
3038
+ const buf = Buffer.from(m[2]!, 'base64')
3039
+ if (buf.length === 0) { writeJson(res, 400, { error: '图片数据为空' }); return }
3040
+ mkdirSync(THEME_BG_DIR, { recursive: true })
3041
+ const name = 'theme-bg-' + Date.now().toString(36) + '-' + randomBytes(3).toString('hex') + '.' + ext
3042
+ writeFileSync(join(THEME_BG_DIR, name), buf)
3043
+ writeJson(res, 200, { url: NOVEL_API.themeBackgroundGet + '/' + name })
3044
+ },
3045
+ }
3046
+
3047
+ const themeBackgroundGetRoute: WebRoute = {
3048
+ kind: 'prefix',
3049
+ path: NOVEL_API.themeBackgroundGet,
3050
+ handler: (req, res) => {
3051
+ if (!guard(req, res, 'GET')) return
3052
+ let pathName = ''
3053
+ try { pathName = new URL(req.url ?? '', 'http://localhost').pathname } catch { /* ignore */ }
3054
+ const name = basename(pathName)
3055
+ if (!name.startsWith('theme-bg-')) { writeJson(res, 404, { error: 'not found' }); return }
3056
+ const file = join(THEME_BG_DIR, name)
3057
+ if (!existsSync(file)) { writeJson(res, 404, { error: 'not found' }); return }
3058
+ const ext = extname(file).slice(1)
3059
+ const type = MIME_BY_EXT[ext] ?? 'application/octet-stream'
3060
+ res.writeHead(200, { 'content-type': type, 'cache-control': 'public, max-age=31536000' })
3061
+ res.end(readFileSync(file))
3062
+ },
3063
+ }
3064
+
2757
3065
  return [
2758
3066
  statusRoute,
2759
3067
  loadOutlineRoute,
@@ -2810,12 +3118,27 @@ export function makeRoutes(deps: NovelRoutesDeps): WebRoute[] {
2810
3118
  styleImageRoute,
2811
3119
  storyboardSkeletonRoute,
2812
3120
  imageTestRoute,
3121
+ llmTestRoute,
3122
+ addModelRoute,
3123
+ llmVendorsRoute,
3124
+ llmModelsRoute,
3125
+ llmProvidersRoute,
3126
+ llmRemoveRoute,
2813
3127
  storyboardTableRoute,
2814
3128
  storyboardPromptsRoute,
2815
3129
  breakdownRoute,
2816
3130
  runStartRoute,
2817
3131
  runControlRoute,
2818
3132
  runStatusRoute,
3133
+ authorAssetsRoute,
3134
+ authorAssetsUpsertRoute,
3135
+ authorAssetsRemoveRoute,
3136
+ authorAssetsImportDefaultRoute,
3137
+ adaptAnalyzeRoute,
3138
+ adaptProposeRoute,
3139
+ adaptExecuteRoute,
3140
+ themeBackgroundUploadRoute,
3141
+ themeBackgroundGetRoute,
2819
3142
  ]
2820
3143
  }
2821
3144