@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.
- package/lib/client.js +3627 -447
- package/lib/client.js.map +1 -1
- package/lib/index.js +1127 -141
- package/lib/index.js.map +1 -1
- package/lib/types/author-assets.d.ts +25 -0
- package/lib/types/client/api.d.ts +31 -1
- package/lib/types/client/locales.d.ts +16 -0
- package/lib/types/client/panel/AdaptModeView.d.ts +4 -0
- package/lib/types/client/panel/AuthorAssetsView.d.ts +4 -0
- package/lib/types/client/panel/AuthorHome.d.ts +30 -0
- package/lib/types/client/panel/ModelManager.d.ts +14 -0
- package/lib/types/client/panel/ModelPicker.d.ts +11 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +17 -0
- package/lib/types/client/panel/ProgressHomeView.d.ts +5 -0
- package/lib/types/client/panel/ReasoningSection.d.ts +10 -0
- package/lib/types/client/panel/SettingsView.d.ts +14 -0
- package/lib/types/client/text.d.ts +9 -0
- package/lib/types/engine.d.ts +39 -1
- package/lib/types/index.d.ts +8 -0
- package/lib/types/protocol.d.ts +339 -0
- package/package.json +1 -1
- package/src/author-assets.ts +166 -0
- package/src/client/api.ts +82 -0
- package/src/client/locales.ts +32 -0
- package/src/client/mount.tsx._new +97 -0
- package/src/client/mount.tsx._nf +97 -0
- package/src/client/mount.tsx._tmp +97 -0
- package/src/client/panel/AdaptModeView.tsx +202 -0
- package/src/client/panel/AuthorAssetsView.tsx +206 -0
- package/src/client/panel/AuthorHome.tsx +107 -0
- package/src/client/panel/ImportModal.tsx +2 -1
- package/src/client/panel/ModelManager.tsx +205 -0
- package/src/client/panel/NovelPanel.tsx +73 -79
- package/src/client/panel/ProgressConsole.tsx +64 -0
- package/src/client/panel/ProgressHomeView.tsx +87 -0
- package/src/client/panel/ReasoningSection.tsx +31 -0
- package/src/client/panel/SettingsView.tsx +292 -0
- package/src/client/panel/panel.module.css +325 -0
- package/src/client/sidebar-entry.ts +1 -1
- package/src/client/text.ts +28 -0
- package/src/engine.ts +442 -0
- package/src/index.ts +20 -0
- package/src/protocol.ts +356 -0
- 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
|
|