@waterwx/dsh-novel-forge 1.7.3 → 1.8.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 +3219 -662
- package/lib/client.js.map +1 -1
- package/lib/index.js +629 -41
- package/lib/index.js.map +1 -1
- package/lib/types/author-assets.d.ts +25 -0
- package/lib/types/client/api.d.ts +18 -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 +20 -0
- package/lib/types/client/panel/ProgressHomeView.d.ts +5 -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 +24 -1
- package/lib/types/index.d.ts +8 -0
- package/lib/types/protocol.d.ts +192 -0
- package/package.json +1 -1
- package/src/author-assets.ts +166 -0
- package/src/client/api.ts +41 -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 +204 -0
- package/src/client/panel/AuthorAssetsView.tsx +206 -0
- package/src/client/panel/AuthorHome.tsx +93 -0
- package/src/client/panel/ImportModal.tsx +2 -1
- package/src/client/panel/NovelPanel.tsx +47 -16
- package/src/client/panel/ProgressHomeView.tsx +87 -0
- package/src/client/panel/SettingsView.tsx +285 -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 +221 -0
- package/src/index.ts +20 -0
- package/src/protocol.ts +196 -0
- package/src/routes.ts +191 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 客户端文件文本智能解码:兼容 UTF-8 BOM / UTF-16 / UTF-8 / GB18030(GBK)。
|
|
3
|
+
* 浏览器 File.text() 固定按 UTF-8 解码,GBK 中文网文 txt 会乱码,这里兜底。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 读取一个 File 的文本,按编码探测兜底解码。
|
|
8
|
+
* @returns 解码后的文本。
|
|
9
|
+
*/
|
|
10
|
+
export async function readFileTextSmart(file: File): Promise<string> {
|
|
11
|
+
const buf = await file.arrayBuffer()
|
|
12
|
+
const u8 = new Uint8Array(buf)
|
|
13
|
+
// UTF-8 BOM
|
|
14
|
+
if (u8.length >= 3 && u8[0] === 0xEF && u8[1] === 0xBB && u8[2] === 0xBF) {
|
|
15
|
+
return new TextDecoder('utf-8').decode(u8.subarray(3))
|
|
16
|
+
}
|
|
17
|
+
// UTF-16 LE / BE
|
|
18
|
+
if (u8.length >= 2 && u8[0] === 0xFF && u8[1] === 0xFE) return new TextDecoder('utf-16le').decode(u8.subarray(2))
|
|
19
|
+
if (u8.length >= 2 && u8[0] === 0xFE && u8[1] === 0xFF) return new TextDecoder('utf-16be').decode(u8.subarray(2))
|
|
20
|
+
// UTF-8(宽松),含替换符则回退 GB18030
|
|
21
|
+
const utf8 = new TextDecoder('utf-8', { fatal: false }).decode(u8)
|
|
22
|
+
if (!utf8.includes('\uFFFD')) return utf8
|
|
23
|
+
try {
|
|
24
|
+
return new TextDecoder('gb18030').decode(u8)
|
|
25
|
+
} catch {
|
|
26
|
+
return utf8
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/engine.ts
CHANGED
|
@@ -46,6 +46,13 @@ import {
|
|
|
46
46
|
emotionZh,
|
|
47
47
|
} from './story-beat-language.ts'
|
|
48
48
|
import type {
|
|
49
|
+
AdaptationDimension,
|
|
50
|
+
AdaptAnalyzeResponse,
|
|
51
|
+
AdaptationMapping,
|
|
52
|
+
AdaptationProposal,
|
|
53
|
+
AdaptationRules,
|
|
54
|
+
AdaptProposeResponse,
|
|
55
|
+
AdaptExecuteResponse,
|
|
49
56
|
AuditIssue,
|
|
50
57
|
AuthorReview,
|
|
51
58
|
BreakdownResponse,
|
|
@@ -3336,6 +3343,220 @@ export async function reverseOutlineFromChapters(
|
|
|
3336
3343
|
return outline.trim()
|
|
3337
3344
|
}
|
|
3338
3345
|
|
|
3346
|
+
/**
|
|
3347
|
+
* 改编模式 P0:全文分析 → 原文设定卡片 / 可改范围矩阵。
|
|
3348
|
+
* 拆章统计 + 取样正文,让 LLM 一次输出结构化 JSON:
|
|
3349
|
+
* { bookName, outline, dimensions: [{key,title,mutability,current,evidence,candidates,impact,risk}] }。
|
|
3350
|
+
*/
|
|
3351
|
+
export async function analyzeAdaptation(
|
|
3352
|
+
ctx: Context,
|
|
3353
|
+
config: NovelConfig,
|
|
3354
|
+
text: string,
|
|
3355
|
+
): Promise<AdaptAnalyzeResponse> {
|
|
3356
|
+
const chapters = splitBookText(text).filter(c => c.body.length >= 50)
|
|
3357
|
+
if (chapters.length === 0) throw new Error('未能从全文拆出章节(内容过短或无章节结构)')
|
|
3358
|
+
|
|
3359
|
+
// 取样:取首/1/3/2/3/尾各章正文节选,控制 token 预算。
|
|
3360
|
+
const sampleBodies: string[] = []
|
|
3361
|
+
const n = chapters.length
|
|
3362
|
+
const pick = (i: number): void => {
|
|
3363
|
+
const c = chapters[i]
|
|
3364
|
+
if (c !== undefined) sampleBodies.push('第' + c.no + '章《' + (c.title || '无题') + '》\n' + c.body.slice(0, 1200))
|
|
3365
|
+
}
|
|
3366
|
+
pick(0)
|
|
3367
|
+
if (n > 3) { pick(Math.floor(n / 3)); pick(Math.floor((2 * n) / 3)) }
|
|
3368
|
+
if (n > 1) pick(n - 1)
|
|
3369
|
+
|
|
3370
|
+
const system = '你是改编策划分析助手。'
|
|
3371
|
+
const user = [
|
|
3372
|
+
'你是一位资深网文编辑兼改编策划。下面给你一部已完结/连载小说的若干章正文节选。请通读并输出该书的「原文设定卡片」与「可改范围矩阵」。',
|
|
3373
|
+
'输出合法 JSON 对象:',
|
|
3374
|
+
'{"bookName": "书名", "outline": "一句话主线梗概(100字内)", "dimensions": [{"key":"realm","title":"大世界","mutability":"big|small|free|locked|visual","current":"当前值","evidence":"证据(出现章节/频次)","candidates":["候选1","候选2"],"impact":"改了会影响什么","risk":"high|medium|low"}]}',
|
|
3375
|
+
'dimensions 至少覆盖以下维度(key/title):realm 大世界、cultivation 修为体系、protagonist 主角、goldenFinger 金手指、supporting 配角与势力人物名、faction 势力/组织、style 文风与叙事、ending 结局走向、timeline 时间线/编年、foreshadow 伏笔/暗线。',
|
|
3376
|
+
'mutability 取值:locked=建议保留、big=可改影响大、small=可改影响小、free=可自由改、visual=仅视觉包装。',
|
|
3377
|
+
'每个 dimension.current 必须忠于文本,能引用原文就用原文(尤其角色名/境界名/势力名/金手指名)。',
|
|
3378
|
+
'重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。',
|
|
3379
|
+
].join('\n')
|
|
3380
|
+
const textOut = await complete(ctx, config, {
|
|
3381
|
+
system,
|
|
3382
|
+
user: user + '\n\n' + sampleBodies.join('\n\n---\n\n'),
|
|
3383
|
+
temperature: 0.3,
|
|
3384
|
+
maxTokens: Math.max(config.maxTokens, 6000),
|
|
3385
|
+
reasoning: config.analysisReasoning ?? 'low',
|
|
3386
|
+
})
|
|
3387
|
+
const raw = parseJsonObject<{ bookName?: unknown; outline?: unknown; dimensions?: unknown }>(textOut)
|
|
3388
|
+
const bookName = typeof raw.bookName === 'string' ? raw.bookName.trim() : '未命名'
|
|
3389
|
+
const dims = Array.isArray(raw.dimensions)
|
|
3390
|
+
? raw.dimensions
|
|
3391
|
+
.filter((d): d is Record<string, unknown> => typeof d === 'object' && d !== null)
|
|
3392
|
+
.map(d => normalizeAdaptationDimension(d))
|
|
3393
|
+
.filter((d): d is AdaptationDimension => d !== null)
|
|
3394
|
+
: []
|
|
3395
|
+
// 反推大纲:独立 LLM 调用,从章节正文节选生成全书总纲(Markdown)。
|
|
3396
|
+
let outline: string | undefined
|
|
3397
|
+
try {
|
|
3398
|
+
outline = await reverseOutlineFromAdaptationText(ctx, config, chapters)
|
|
3399
|
+
} catch {
|
|
3400
|
+
outline = typeof raw.outline === 'string' && raw.outline.trim() !== '' ? raw.outline.trim() : undefined
|
|
3401
|
+
}
|
|
3402
|
+
return {
|
|
3403
|
+
bookName,
|
|
3404
|
+
chapters: chapters.length,
|
|
3405
|
+
outline,
|
|
3406
|
+
dimensions: dims,
|
|
3407
|
+
note: '基于节选分析(首/中/末取样)+ 反推大纲。如需逐章全文级深度分析请后续启用全文流式入口。',
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
|
|
3411
|
+
/** 校验并归一化一行的改编维度数据(来自 LLM)。 */
|
|
3412
|
+
function normalizeAdaptationDimension(d: Record<string, unknown>): AdaptationDimension | null {
|
|
3413
|
+
const key = typeof d.key === 'string' ? d.key : ''
|
|
3414
|
+
const title = typeof d.title === 'string' ? d.title : ''
|
|
3415
|
+
if (key === '' || title === '') return null
|
|
3416
|
+
const mutability = (['locked', 'big', 'small', 'free', 'visual'] as const).includes(d.mutability as AdaptationDimension['mutability'])
|
|
3417
|
+
? d.mutability as AdaptationDimension['mutability']
|
|
3418
|
+
: 'small'
|
|
3419
|
+
const risk = (['high', 'medium', 'low'] as const).includes(d.risk as AdaptationDimension['risk'])
|
|
3420
|
+
? d.risk as AdaptationDimension['risk']
|
|
3421
|
+
: 'medium'
|
|
3422
|
+
return {
|
|
3423
|
+
key,
|
|
3424
|
+
title,
|
|
3425
|
+
mutability,
|
|
3426
|
+
current: typeof d.current === 'string' ? d.current : '',
|
|
3427
|
+
evidence: typeof d.evidence === 'string' ? d.evidence : undefined,
|
|
3428
|
+
candidates: Array.isArray(d.candidates) ? d.candidates.filter((x): x is string => typeof x === 'string') : [],
|
|
3429
|
+
impact: typeof d.impact === 'string' ? d.impact : '',
|
|
3430
|
+
risk,
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
|
|
3434
|
+
/** 从全文拆出的章节节选反推全书总纲(Markdown),用于改编 P0 的「反推大纲」。 */
|
|
3435
|
+
async function reverseOutlineFromAdaptationText(
|
|
3436
|
+
ctx: Context,
|
|
3437
|
+
config: NovelConfig,
|
|
3438
|
+
chapters: Array<{ no: number; title: string; body: string }>,
|
|
3439
|
+
): Promise<string> {
|
|
3440
|
+
// 均匀采样最多 20 章,每章正文前 600 字,控制成本。
|
|
3441
|
+
const n = chapters.length
|
|
3442
|
+
const sample: string[] = []
|
|
3443
|
+
const count = Math.min(n, 20)
|
|
3444
|
+
for (let i = 0; i < count; i++) {
|
|
3445
|
+
const idx = Math.floor((i * n) / count)
|
|
3446
|
+
const c = chapters[idx]
|
|
3447
|
+
if (c === undefined) continue
|
|
3448
|
+
sample.push('第' + c.no + '章《' + (c.title || '无题') + '》\n' + c.body.slice(0, 600))
|
|
3449
|
+
}
|
|
3450
|
+
const system = [
|
|
3451
|
+
'你是一位经验丰富的小说主编。根据下面若干章节的正文节选,反推出这本书的总纲(可作为后续改编与章节续写的骨架)。',
|
|
3452
|
+
'要求:',
|
|
3453
|
+
'1. 第一行写《书名》(可从正文推断,否则《未命名》)。',
|
|
3454
|
+
'2. 按故事弧线划分卷/部分:每卷给卷名与主旨(标注覆盖章节范围)。',
|
|
3455
|
+
'3. 对每章列出:章节号 + 标题 + 一句话核心情节。',
|
|
3456
|
+
'4. 最后给出:全书主线、主要人物弧线、已埋设待回收的伏笔清单。',
|
|
3457
|
+
'5. 输出纯文本 Markdown 结构(# 一级标题、## 二级标题、- 列表),不要寒暄,不要其他输出。',
|
|
3458
|
+
].join('\n')
|
|
3459
|
+
const user = sample.join('\n\n---\n\n')
|
|
3460
|
+
const text = await complete(ctx, config, { system, user, temperature: 0.4, maxTokens: Math.max(config.maxTokens, 6000), reasoning: config.analysisReasoning ?? 'low' })
|
|
3461
|
+
return text.trim()
|
|
3462
|
+
}
|
|
3463
|
+
|
|
3464
|
+
/** 改编方案:由用户勾选的维度与新值,生成 LLM 映射表/规则/影响清单。 */
|
|
3465
|
+
export async function proposeAdaptation(
|
|
3466
|
+
ctx: Context,
|
|
3467
|
+
config: NovelConfig,
|
|
3468
|
+
text: string,
|
|
3469
|
+
selections: Array<{ key: string; title: string; current: string; target: string; mutability: string }>,
|
|
3470
|
+
dimensions?: AdaptationDimension[],
|
|
3471
|
+
): Promise<AdaptProposeResponse> {
|
|
3472
|
+
const selLines = selections.map(s => '- ' + s.title + '(' + s.key + '):' + s.current + ' → ' + s.target).join('\n')
|
|
3473
|
+
const dimLines = (dimensions ?? []).map(d => '- ' + d.title + ':' + d.current + '(可改度:' + d.mutability + ',风险:' + d.risk + ')').join('\n')
|
|
3474
|
+
const system = '你是改编策划。'
|
|
3475
|
+
const user = [
|
|
3476
|
+
'下面给出改编决策:用户想改哪些维度、改成什么值。请生成一份可执行的「改编方案」。',
|
|
3477
|
+
'要求输出合法 JSON 对象:',
|
|
3478
|
+
'{"mappings": [{"source":"原值","target":"新值","scope":"name|realm|faction|term|other","note":"说明"}], "rules": {"preserve":["必须保留的要素"],"change":["允许改变的要素"],"constraints":["改编红线/一致性要求"]}, "impacts": [{"item":"受影响项","detail":"说明","risk":"high|medium|low","chapters":[章号]}]}',
|
|
3479
|
+
'mappings 需把用户确认的新值展开为「原→新」条目(如主角名/境界名/势力名/术语),并补充用户未填但关联的必改项(如改了修为体系名,相关的境界名一并列映射)。',
|
|
3480
|
+
'rules.preserve 至少包含:故事骨架、人物动机、伏笔逻辑、爽点结构。',
|
|
3481
|
+
'impacts 列出每个改动会影响的内容(术语/角色/章节/伏笔),能定位章号尽量定位;无法定位则给章节区间提示。',
|
|
3482
|
+
'重要:所有字符串值内部不得包含换行符;JSON 必须在一段内完整结束;直接输出 JSON,不要 Markdown 代码块。',
|
|
3483
|
+
'',
|
|
3484
|
+
'用户要改:',
|
|
3485
|
+
selLines,
|
|
3486
|
+
'',
|
|
3487
|
+
'原文可改矩阵(已分析):',
|
|
3488
|
+
dimLines,
|
|
3489
|
+
].join('\n')
|
|
3490
|
+
const out = await complete(ctx, config, { system, user, temperature: 0.3, maxTokens: Math.max(config.maxTokens, 6000), reasoning: config.analysisReasoning ?? 'low' })
|
|
3491
|
+
const raw = parseJsonObject<{ mappings?: unknown; rules?: unknown; impacts?: unknown }>(out)
|
|
3492
|
+
const mappings = Array.isArray(raw.mappings)
|
|
3493
|
+
? raw.mappings
|
|
3494
|
+
.filter((m): m is Record<string, unknown> => typeof m === 'object' && m !== null)
|
|
3495
|
+
.map(m => normalizeAdaptationMapping(m))
|
|
3496
|
+
.filter((m): m is AdaptationMapping => m !== null)
|
|
3497
|
+
: []
|
|
3498
|
+
const rules = normalizeAdaptationRules(raw.rules)
|
|
3499
|
+
const impacts = Array.isArray(raw.impacts)
|
|
3500
|
+
? raw.impacts
|
|
3501
|
+
.filter((i): i is Record<string, unknown> => typeof i === 'object' && i !== null)
|
|
3502
|
+
.map(i => normalizeAdaptationImpact(i))
|
|
3503
|
+
: []
|
|
3504
|
+
return { proposal: { mappings, rules, impacts } }
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
/** 剧本术语替换执行:按映射表做精确替换并统计命中。 */
|
|
3508
|
+
export function applyAdaptationReplacements(
|
|
3509
|
+
text: string,
|
|
3510
|
+
mappings: AdaptationMapping[],
|
|
3511
|
+
): { adaptedText: string; hits: Array<{ source: string; target: string; count: number }> } {
|
|
3512
|
+
const seen = new Set<string>()
|
|
3513
|
+
const unique = mappings.filter(m => {
|
|
3514
|
+
const s = m.source.trim()
|
|
3515
|
+
if (s === '' || s === m.target.trim()) return false
|
|
3516
|
+
if (seen.has(s)) return false
|
|
3517
|
+
seen.add(s)
|
|
3518
|
+
return true
|
|
3519
|
+
}).sort((a, b) => b.source.length - a.source.length)
|
|
3520
|
+
let adapted = text
|
|
3521
|
+
const hits: Array<{ source: string; target: string; count: number }> = []
|
|
3522
|
+
for (const m of unique) {
|
|
3523
|
+
const count = adapted.split(m.source).length - 1
|
|
3524
|
+
if (count > 0) adapted = adapted.split(m.source).join(m.target)
|
|
3525
|
+
hits.push({ source: m.source, target: m.target, count })
|
|
3526
|
+
}
|
|
3527
|
+
return { adaptedText: adapted, hits }
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
/** 校验归一化一条映射(来自 LLM)。 */
|
|
3531
|
+
function normalizeAdaptationMapping(m: Record<string, unknown>): AdaptationMapping | null {
|
|
3532
|
+
const source = typeof m.source === 'string' ? m.source.trim() : ''
|
|
3533
|
+
const target = typeof m.target === 'string' ? m.target.trim() : ''
|
|
3534
|
+
if (source === '' || target === '') return null
|
|
3535
|
+
const scope = (['name', 'realm', 'faction', 'term', 'other'] as const).includes(m.scope as AdaptationMapping['scope'])
|
|
3536
|
+
? m.scope as AdaptationMapping['scope']
|
|
3537
|
+
: 'other'
|
|
3538
|
+
return { source, target, scope, note: typeof m.note === 'string' ? m.note : undefined }
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
/** 校验归一化改编规则(来自 LLM)。 */
|
|
3542
|
+
function normalizeAdaptationRules(r: unknown): AdaptationRules {
|
|
3543
|
+
const obj = typeof r === 'object' && r !== null ? r as Record<string, unknown> : {}
|
|
3544
|
+
const arr = (v: unknown): string[] => Array.isArray(v) ? v.filter((x): x is string => typeof x === 'string') : []
|
|
3545
|
+
return { preserve: arr(obj.preserve), change: arr(obj.change), constraints: arr(obj.constraints) }
|
|
3546
|
+
}
|
|
3547
|
+
|
|
3548
|
+
/** 校验归一化一条影响项(来自 LLM)。 */
|
|
3549
|
+
function normalizeAdaptationImpact(i: Record<string, unknown>): { item: string; detail: string; risk: 'high' | 'medium' | 'low'; chapters?: number[] } {
|
|
3550
|
+
const risk = (['high', 'medium', 'low'] as const).includes(i.risk as 'high' | 'medium' | 'low') ? i.risk as 'high' | 'medium' | 'low' : 'medium'
|
|
3551
|
+
const chapters = Array.isArray(i.chapters) ? i.chapters.filter((x): x is number => typeof x === 'number') : undefined
|
|
3552
|
+
return {
|
|
3553
|
+
item: typeof i.item === 'string' ? i.item : '',
|
|
3554
|
+
detail: typeof i.detail === 'string' ? i.detail : '',
|
|
3555
|
+
risk,
|
|
3556
|
+
chapters: chapters !== undefined && chapters.length > 0 ? chapters : undefined,
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3339
3560
|
/**
|
|
3340
3561
|
* 摘要 + 事实抽取合并为一次 LLM 调用(省一次调用与一次正文输入,
|
|
3341
3562
|
* 批量生成时整体开销约省 25%)。
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,14 @@ export interface Config {
|
|
|
70
70
|
imageApiModel?: string
|
|
71
71
|
/** 是否启用豆包生图(旧字段,兼容迁移用)。 */
|
|
72
72
|
imageApiEnabled?: boolean
|
|
73
|
+
/** 自定义背景图(URL / dataURL / 服务端路径引用)。 */
|
|
74
|
+
themeBackground?: string
|
|
75
|
+
/** 自定义背景遮罩/模糊强度 0-80。 */
|
|
76
|
+
themeBackgroundBlur?: number
|
|
77
|
+
/** 玻璃透明度 0-100(100=当前原样)。 */
|
|
78
|
+
themeOpacity?: number
|
|
79
|
+
/** 是否启用改编模式(默认关闭)。 */
|
|
80
|
+
enableAdaptMode?: boolean
|
|
73
81
|
}
|
|
74
82
|
|
|
75
83
|
export const Config: z<Config> = z.object({
|
|
@@ -98,6 +106,10 @@ export const Config: z<Config> = z.object({
|
|
|
98
106
|
imageApiKey: z.string().default(''),
|
|
99
107
|
imageApiModel: z.string().default(''),
|
|
100
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(false),
|
|
101
113
|
})
|
|
102
114
|
|
|
103
115
|
/** Schema defaults, re-read for hand-built test contexts. */
|
|
@@ -157,6 +169,10 @@ export function resolveConfig(value: Partial<Config> | undefined): NovelConfig {
|
|
|
157
169
|
return active?.model ?? value?.imageApiModel
|
|
158
170
|
})(),
|
|
159
171
|
imageApiEnabled: value?.imageApiEnabled ?? false,
|
|
172
|
+
themeBackground: value?.themeBackground ?? '',
|
|
173
|
+
themeBackgroundBlur: value?.themeBackgroundBlur ?? 0,
|
|
174
|
+
themeOpacity: value?.themeOpacity ?? 100,
|
|
175
|
+
enableAdaptMode: value?.enableAdaptMode ?? false,
|
|
160
176
|
}
|
|
161
177
|
}
|
|
162
178
|
|
|
@@ -199,6 +215,10 @@ export function apply(ctx: Context, config?: Config): void {
|
|
|
199
215
|
if (patch.imageApiKey !== undefined) next.imageApiKey = patch.imageApiKey
|
|
200
216
|
if (patch.imageApiModel !== undefined) next.imageApiModel = patch.imageApiModel
|
|
201
217
|
if (patch.imageApiEnabled !== undefined) next.imageApiEnabled = patch.imageApiEnabled
|
|
218
|
+
if (patch.themeBackground !== undefined) next.themeBackground = patch.themeBackground
|
|
219
|
+
if (patch.themeBackgroundBlur !== undefined) next.themeBackgroundBlur = patch.themeBackgroundBlur
|
|
220
|
+
if (patch.themeOpacity !== undefined) next.themeOpacity = patch.themeOpacity
|
|
221
|
+
if (patch.enableAdaptMode !== undefined) next.enableAdaptMode = patch.enableAdaptMode
|
|
202
222
|
if (patch.imageModels !== undefined) next.imageModels = patch.imageModels
|
|
203
223
|
// Persist through the settings seam when available; otherwise keep in memory.
|
|
204
224
|
// (ctx.get is the non-strict service access — no inject requirement, same
|
package/src/protocol.ts
CHANGED
|
@@ -113,6 +113,24 @@ export const NOVEL_API = {
|
|
|
113
113
|
runStatus: '/api/dsh-novel-forge/run/status',
|
|
114
114
|
config: '/api/dsh-novel-forge/config',
|
|
115
115
|
openFolder: '/api/dsh-novel-forge/open-folder',
|
|
116
|
+
/** 作者资产库/总数据:读取个人跨书资产(笔法/红线/套路/角色模板/世界观模板)。 */
|
|
117
|
+
authorAssets: '/api/dsh-novel-forge/author-assets',
|
|
118
|
+
/** 作者资产库:新增/更新一条资产(upsert by id)。 */
|
|
119
|
+
authorAssetsUpsert: '/api/dsh-novel-forge/author-assets/upsert',
|
|
120
|
+
/** 作者资产库:删除一条资产。 */
|
|
121
|
+
authorAssetsRemove: '/api/dsh-novel-forge/author-assets/remove',
|
|
122
|
+
/** 作者资产库:导入默认(书架书的写作资产/角色 + 内置全局库)批量沉淀。 */
|
|
123
|
+
authorAssetsImportDefault: '/api/dsh-novel-forge/author-assets/import-default',
|
|
124
|
+
/** 改编模式:上传全文 → 分析 → 原文设定卡片/可改范围矩阵。 */
|
|
125
|
+
adaptAnalyze: '/api/dsh-novel-forge/adapt/analyze',
|
|
126
|
+
/** 改编模式:确认要改的维度 → 生成映射表/改编规则/联动影响清单。 */
|
|
127
|
+
adaptPropose: '/api/dsh-novel-forge/adapt/propose',
|
|
128
|
+
/** 改编模式:执行术语替换(全局替换 + 命中统计 + 改编文本预览)。 */
|
|
129
|
+
adaptExecute: '/api/dsh-novel-forge/adapt/execute',
|
|
130
|
+
/** 主题自定义背景:上传图片(POST,存盘并返回服务端 URL)。 */
|
|
131
|
+
themeBackgroundUpload: '/api/dsh-novel-forge/theme/background',
|
|
132
|
+
/** 主题自定义背景:读取已上传文件(GET prefix,/theme/background/<name>)。 */
|
|
133
|
+
themeBackgroundGet: '/api/dsh-novel-forge/theme/background',
|
|
116
134
|
} as const
|
|
117
135
|
|
|
118
136
|
/** 书架:一本书的条目。 */
|
|
@@ -1252,6 +1270,14 @@ export interface NovelConfig {
|
|
|
1252
1270
|
imageApiModel?: string
|
|
1253
1271
|
/** 是否启用生图(= 存在启用条目)。 */
|
|
1254
1272
|
imageApiEnabled?: boolean
|
|
1273
|
+
/** 自定义背景图(URL 或 dataURL 或服务端路径引用;空 = 使用主题默认背景)。 */
|
|
1274
|
+
themeBackground?: string
|
|
1275
|
+
/** 自定义背景遮罩/模糊强度 0-80(0 = 不遮罩)。 */
|
|
1276
|
+
themeBackgroundBlur?: number
|
|
1277
|
+
/** 玻璃透明度 0-100(100=当前原样,越小越透,新拟态/黏土可见背景图)。 */
|
|
1278
|
+
themeOpacity?: number
|
|
1279
|
+
/** 是否启用改编模式(默认关闭;发布后开启)。 */
|
|
1280
|
+
enableAdaptMode?: boolean
|
|
1255
1281
|
}
|
|
1256
1282
|
|
|
1257
1283
|
/** GET /status response. */
|
|
@@ -1468,6 +1494,13 @@ export interface ConfigPatch {
|
|
|
1468
1494
|
imageApiKey?: string
|
|
1469
1495
|
imageApiModel?: string
|
|
1470
1496
|
imageApiEnabled?: boolean
|
|
1497
|
+
/** 自定义背景图(URL / dataURL / 服务端路径引用)。 */
|
|
1498
|
+
themeBackground?: string
|
|
1499
|
+
/** 自定义背景遮罩/模糊强度 0-80。 */
|
|
1500
|
+
themeBackgroundBlur?: number
|
|
1501
|
+
/** 玻璃透明度 0-100(100=当前原样)。 */
|
|
1502
|
+
themeOpacity?: number
|
|
1503
|
+
enableAdaptMode?: boolean
|
|
1471
1504
|
/** 生图模型库(完整替换保存)。 */
|
|
1472
1505
|
imageModels?: ImageModelConfig[]
|
|
1473
1506
|
}
|
|
@@ -1633,3 +1666,166 @@ export interface StyleEngineRequest {
|
|
|
1633
1666
|
/** 资产名(可选,默认「风格资产 N」)。 */
|
|
1634
1667
|
name?: string
|
|
1635
1668
|
}
|
|
1669
|
+
|
|
1670
|
+
// ---------------------------------------------------------- author assets (总数据)
|
|
1671
|
+
|
|
1672
|
+
/** 作者资产库中一条可跨书复用的资产(笔法/红线/套路/角色模板/世界观模板等)。 */
|
|
1673
|
+
export interface AuthorStyleAsset {
|
|
1674
|
+
/** 稳定 id(本地唯一)。 */
|
|
1675
|
+
id: string
|
|
1676
|
+
/** 资产名(如「短句快节奏爽文风」「林越式痞坏角色」)。 */
|
|
1677
|
+
name: string
|
|
1678
|
+
/** 资产类型。 */
|
|
1679
|
+
kind: 'style' | 'antiAi' | 'progression' | 'genre' | 'roleTemplate' | 'worldTemplate' | 'custom'
|
|
1680
|
+
/** 一句话摘要。 */
|
|
1681
|
+
summary: string
|
|
1682
|
+
/** 资产正文内容(按 kind 约定解读的文本/JSON 描述)。 */
|
|
1683
|
+
content: string
|
|
1684
|
+
/** 结构化载荷(角色模板/世界观模板等可结构化字段;缺省为 null)。 */
|
|
1685
|
+
structured?: Record<string, unknown>
|
|
1686
|
+
/** 来源书名(哪本书提炼/收藏而来)。 */
|
|
1687
|
+
sourceBooks: string[]
|
|
1688
|
+
/** 标签。 */
|
|
1689
|
+
tags: string[]
|
|
1690
|
+
/** 来源样本文本(可选)。 */
|
|
1691
|
+
sourceText?: string
|
|
1692
|
+
/** 创建时间。 */
|
|
1693
|
+
createdAt: string
|
|
1694
|
+
/** 更新时间。 */
|
|
1695
|
+
updatedAt: string
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
/** 作者资产库(跨书总数据):按作者维度聚合的可复用资产。 */
|
|
1699
|
+
export interface AuthorAssetLibrary {
|
|
1700
|
+
/** 货架版本,便于未来迁移。 */
|
|
1701
|
+
version: 1
|
|
1702
|
+
items: AuthorStyleAsset[]
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
/** GET /author-assets 响应。 */
|
|
1706
|
+
export interface AuthorAssetsResponse {
|
|
1707
|
+
assets: AuthorAssetLibrary
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
/** POST /author-assets/upsert 请求:新增或按 id 更新一条资产。 */
|
|
1711
|
+
export interface AuthorAssetUpsertRequest {
|
|
1712
|
+
asset: AuthorStyleAsset
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
/** POST /author-assets/remove 请求:删除一条资产。 */
|
|
1716
|
+
export interface AuthorAssetRemoveRequest {
|
|
1717
|
+
id: string
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
// ---------------------------------------------------------- adaptation (改编模式)
|
|
1721
|
+
|
|
1722
|
+
/** 改编可改范围的单个维度(矩阵的一行)。 */
|
|
1723
|
+
export interface AdaptationDimension {
|
|
1724
|
+
/** 维度 key(如 realm/name/goldenFinger…)。 */
|
|
1725
|
+
key: string
|
|
1726
|
+
/** 维度名(如「大世界」「修为体系」「主角名」「金手指」)。 */
|
|
1727
|
+
title: string
|
|
1728
|
+
/** 可改度:保留 / 可改影响大 / 可改影响小 / 可自由改 / 仅视觉包装。 */
|
|
1729
|
+
mutability: 'locked' | 'big' | 'small' | 'free' | 'visual'
|
|
1730
|
+
/** 原文当前值(从正文提炼,附证据)。 */
|
|
1731
|
+
current: string
|
|
1732
|
+
/** 该值在正文中的出现证据(章节/频次,可空)。 */
|
|
1733
|
+
evidence?: string
|
|
1734
|
+
/** AI 建议的候选新值(可空)。 */
|
|
1735
|
+
candidates?: string[]
|
|
1736
|
+
/** 联动影响说明(改了会影响哪些章节/角色/伏笔/术语)。 */
|
|
1737
|
+
impact: string
|
|
1738
|
+
/** 风险评级。 */
|
|
1739
|
+
risk: 'high' | 'medium' | 'low'
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
/** POST /adapt/analyze 请求:上传全文做改编分析。 */
|
|
1743
|
+
export interface AdaptAnalyzeRequest {
|
|
1744
|
+
/** 全文文本(优先)。 */
|
|
1745
|
+
text: string
|
|
1746
|
+
/** 或服务器端全文文件路径。 */
|
|
1747
|
+
filePath?: string
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
/** POST /adapt/analyze 响应。 */
|
|
1751
|
+
export interface AdaptAnalyzeResponse {
|
|
1752
|
+
/** 识别书名(文件名推断或正文首行)。 */
|
|
1753
|
+
bookName: string
|
|
1754
|
+
/** 拆出的章节数。 */
|
|
1755
|
+
chapters: number
|
|
1756
|
+
/** 反推的初始大纲(可空)。 */
|
|
1757
|
+
outline?: string
|
|
1758
|
+
/** 可改范围矩阵。 */
|
|
1759
|
+
dimensions: AdaptationDimension[]
|
|
1760
|
+
/** 分析消耗说明。 */
|
|
1761
|
+
note?: string
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
/** 一条改编映射(原值 → 新值)。 */
|
|
1765
|
+
export interface AdaptationMapping {
|
|
1766
|
+
/** 原值(如「林尘」「练气」「青云宗」)。 */
|
|
1767
|
+
source: string
|
|
1768
|
+
/** 新值(如「楚风」「凝元」「天渊阁」)。 */
|
|
1769
|
+
target: string
|
|
1770
|
+
/** 适用范围。 */
|
|
1771
|
+
scope: 'name' | 'realm' | 'faction' | 'term' | 'other'
|
|
1772
|
+
/** 说明(可空)。 */
|
|
1773
|
+
note?: string
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
/** 改编规则:哪些必须保留、哪些允许改、一致性约束。 */
|
|
1777
|
+
export interface AdaptationRules {
|
|
1778
|
+
/** 必须保留的要素(骨架/人物动机/伏笔逻辑/爽点结构…)。 */
|
|
1779
|
+
preserve: string[]
|
|
1780
|
+
/** 允许改变的要素。 */
|
|
1781
|
+
change: string[]
|
|
1782
|
+
/** 改编红线/一致性要求。 */
|
|
1783
|
+
constraints: string[]
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
/** 一组改编方案(映射表 + 规则 + 联动影响清单)。 */
|
|
1787
|
+
export interface AdaptationProposal {
|
|
1788
|
+
/** 映射表(原→新)。 */
|
|
1789
|
+
mappings: AdaptationMapping[]
|
|
1790
|
+
/** 改编规则。 */
|
|
1791
|
+
rules: AdaptationRules
|
|
1792
|
+
/** 联动影响清单。 */
|
|
1793
|
+
impacts: Array<{ item: string; detail: string; risk: 'high' | 'medium' | 'low'; chapters?: number[] }>
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
/** POST /adapt/propose 请求:由用户勾选的维度 + 新值生成改编方案。 */
|
|
1797
|
+
export interface AdaptProposeRequest {
|
|
1798
|
+
/** 原文全文(复用,便于 LLM 核对联动影响)。 */
|
|
1799
|
+
text: string
|
|
1800
|
+
/** 用户确认要改的维度(含原值与新值)。 */
|
|
1801
|
+
selections: Array<{ key: string; title: string; current: string; target: string; mutability: string }>
|
|
1802
|
+
/** 若已分析过,可携带完整矩阵作为上下文。 */
|
|
1803
|
+
dimensions?: AdaptationDimension[]
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/** POST /adapt/propose 响应。 */
|
|
1807
|
+
export interface AdaptProposeResponse {
|
|
1808
|
+
proposal: AdaptationProposal
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
/** POST /adapt/execute 请求:执行改编(首版为术语替换)。 */
|
|
1812
|
+
export interface AdaptExecuteRequest {
|
|
1813
|
+
/** 原文全文。 */
|
|
1814
|
+
text: string
|
|
1815
|
+
/** 改编映射表。 */
|
|
1816
|
+
mappings: AdaptationMapping[]
|
|
1817
|
+
/** 改编规则(可选,后续逐章改写使用;替换模式暂不依赖)。 */
|
|
1818
|
+
rules?: AdaptationRules
|
|
1819
|
+
/** 执行模式:首版仅 replace(术语替换)。 */
|
|
1820
|
+
mode?: 'replace' | 'rewrite'
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
/** POST /adapt/execute 响应。 */
|
|
1824
|
+
export interface AdaptExecuteResponse {
|
|
1825
|
+
/** 改编后的全文。 */
|
|
1826
|
+
adaptedText: string
|
|
1827
|
+
/** 执行的映射数。 */
|
|
1828
|
+
mappings: number
|
|
1829
|
+
/** 命中统计:每条映射的替换次数。 */
|
|
1830
|
+
hits: Array<{ source: string; target: string; count: number }>
|
|
1831
|
+
}
|