@waterwx/dsh-novel-forge 2.1.2-alpha → 2.1.2
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 +278 -368
- package/lib/client.js +19568 -21381
- package/lib/index.js +4850 -4002
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +66 -12
- package/lib/types/assets.d.ts +7 -9
- package/lib/types/assistant.d.ts +25 -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 +49 -29
- 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 +12 -14
- package/lib/types/client/locales.d.ts +12 -67
- package/lib/types/client/mount.d.ts +0 -2
- package/lib/types/client/panel/AdaptModeView.d.ts +1 -1
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssetsTab.d.ts +1 -1
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorAssetsView.d.ts +1 -1
- package/lib/types/client/panel/AuthorHome.d.ts +12 -12
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +1 -1
- package/lib/types/client/panel/GlobalAssetLibraryView.d.ts +1 -1
- package/lib/types/client/panel/ImportModal.d.ts +1 -1
- package/lib/types/client/panel/LiveFeedLog.d.ts +8 -0
- package/lib/types/client/panel/ModelManager.d.ts +2 -2
- package/lib/types/client/panel/NovelPanel.d.ts +7 -2
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/ProgressConsole.d.ts +1 -1
- package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/ReaderView.d.ts +1 -1
- package/lib/types/client/panel/RunPanel.d.ts +1 -1
- package/lib/types/client/panel/SettingsView.d.ts +13 -10
- package/lib/types/client/panel/ShelfView.d.ts +11 -2
- 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/WorldTab.d.ts +1 -1
- 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/outline-markdown.d.ts +112 -0
- package/lib/types/client/panel/status-badge.d.ts +13 -0
- package/lib/types/client/panel/storage-parse.d.ts +46 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/usePersistentState.d.ts +17 -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/digest-parse.d.ts +29 -0
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +122 -103
- package/lib/types/home.d.ts +11 -0
- package/lib/types/index.d.ts +21 -13
- package/lib/types/llm-live.d.ts +62 -36
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +0 -1
- package/lib/types/market-radar-sources.d.ts +1 -1
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/model-capability.d.ts +119 -0
- package/lib/types/model-request.d.ts +36 -0
- package/lib/types/output-budget.d.ts +86 -0
- package/lib/types/plotline-align.d.ts +35 -0
- package/lib/types/prompt-slots.d.ts +57 -0
- package/lib/types/prompting.d.ts +2 -27
- package/lib/types/protocol.d.ts +479 -595
- package/lib/types/revision.d.ts +76 -0
- package/lib/types/routes.d.ts +9 -3
- package/lib/types/run-selection.d.ts +51 -0
- package/lib/types/run.d.ts +31 -0
- 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/stream-fallback.d.ts +60 -0
- package/lib/types/strip-headings.d.ts +33 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +41 -28
- 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/src/ai-scan.d.ts +0 -36
- package/lib/types/src/assets.d.ts +0 -37
- package/lib/types/src/assistant.d.ts +0 -45
- package/lib/types/src/author-assets.d.ts +0 -25
- package/lib/types/src/bookshelf.d.ts +0 -42
- package/lib/types/src/client/api.d.ts +0 -234
- package/lib/types/src/client/docx.d.ts +0 -15
- package/lib/types/src/client/index.d.ts +0 -14
- package/lib/types/src/client/locales.d.ts +0 -212
- package/lib/types/src/client/mount.d.ts +0 -9
- package/lib/types/src/client/panel/AdaptModeView.d.ts +0 -5
- package/lib/types/src/client/panel/AssetsTab.d.ts +0 -12
- package/lib/types/src/client/panel/AssistantTab.d.ts +0 -7
- package/lib/types/src/client/panel/AuthorAssetsView.d.ts +0 -4
- package/lib/types/src/client/panel/AuthorHome.d.ts +0 -32
- package/lib/types/src/client/panel/BookshelfBar.d.ts +0 -11
- package/lib/types/src/client/panel/CreateBookView.d.ts +0 -9
- package/lib/types/src/client/panel/FlowGuide.d.ts +0 -22
- package/lib/types/src/client/panel/GlobalAssetLibraryView.d.ts +0 -4
- package/lib/types/src/client/panel/ImportModal.d.ts +0 -7
- package/lib/types/src/client/panel/MangaRoleLibrary.d.ts +0 -20
- package/lib/types/src/client/panel/MangaWorkspace.d.ts +0 -12
- package/lib/types/src/client/panel/ModelManager.d.ts +0 -14
- package/lib/types/src/client/panel/NovelPanel.d.ts +0 -13
- package/lib/types/src/client/panel/ProgressConsole.d.ts +0 -17
- package/lib/types/src/client/panel/ProgressHomeView.d.ts +0 -5
- package/lib/types/src/client/panel/PropLibrary.d.ts +0 -8
- package/lib/types/src/client/panel/ReaderView.d.ts +0 -8
- package/lib/types/src/client/panel/ReasoningSection.d.ts +0 -10
- package/lib/types/src/client/panel/RunPanel.d.ts +0 -5
- package/lib/types/src/client/panel/SceneLibrary.d.ts +0 -16
- package/lib/types/src/client/panel/SettingsView.d.ts +0 -16
- package/lib/types/src/client/panel/ShelfView.d.ts +0 -15
- package/lib/types/src/client/panel/StoryboardTab.d.ts +0 -19
- package/lib/types/src/client/panel/StyleCard.d.ts +0 -8
- package/lib/types/src/client/panel/StyleGallery.d.ts +0 -1
- package/lib/types/src/client/panel/WorldTab.d.ts +0 -10
- package/lib/types/src/client/panel/controller.d.ts +0 -19
- package/lib/types/src/client/panel/helpers.d.ts +0 -18
- package/lib/types/src/client/panel/views.d.ts +0 -77
- package/lib/types/src/client/sidebar-entry.d.ts +0 -13
- package/lib/types/src/client/text.d.ts +0 -9
- package/lib/types/src/comic-presets.d.ts +0 -12
- package/lib/types/src/docx.d.ts +0 -19
- package/lib/types/src/engine.d.ts +0 -402
- package/lib/types/src/index.d.ts +0 -79
- package/lib/types/src/manga-genre-rules.d.ts +0 -22
- package/lib/types/src/novel-context.d.ts +0 -80
- package/lib/types/src/prompt-utils.d.ts +0 -35
- package/lib/types/src/protocol.d.ts +0 -2176
- package/lib/types/src/review-policy.d.ts +0 -97
- package/lib/types/src/routes.d.ts +0 -29
- package/lib/types/src/run.d.ts +0 -32
- package/lib/types/src/shot-language.d.ts +0 -46
- package/lib/types/src/story-beat-language.d.ts +0 -35
- package/lib/types/src/style-library.d.ts +0 -33
- package/lib/types/story-beat-language.d.ts +0 -35
- package/lib/types/style-library.d.ts +0 -33
- package/src/ai-scan.ts +0 -179
- package/src/assets.ts +0 -1774
- package/src/assistant.ts +0 -915
- package/src/author-assets.ts +0 -147
- package/src/bookshelf.ts +0 -167
- package/src/client/api.ts +0 -716
- package/src/client/css-modules.d.ts +0 -8
- package/src/client/docx.ts +0 -69
- package/src/client/index.ts +0 -34
- package/src/client/llmLive.ts +0 -101
- package/src/client/locales.ts +0 -418
- package/src/client/mount.tsx +0 -97
- 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/AdaptModeView.tsx +0 -535
- package/src/client/panel/AssetsTab.tsx +0 -458
- package/src/client/panel/AssistantTab.tsx +0 -339
- package/src/client/panel/AuthorAssetsView.tsx +0 -208
- package/src/client/panel/AuthorHome.tsx +0 -234
- package/src/client/panel/BookAnalysisView.tsx +0 -60
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/CreateBookView.tsx +0 -371
- package/src/client/panel/DirectorView.tsx +0 -200
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/GlobalAssetLibraryView.tsx +0 -172
- package/src/client/panel/IdeaInspirationView.tsx +0 -71
- package/src/client/panel/ImportModal.tsx +0 -303
- package/src/client/panel/KnowledgeBaseView.tsx +0 -73
- package/src/client/panel/LiveFeedLog.tsx +0 -76
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/MarketRadarView.tsx +0 -374
- package/src/client/panel/ModelManager.tsx +0 -205
- package/src/client/panel/NovelPanel.tsx +0 -4721
- package/src/client/panel/ProgressConsole.tsx +0 -63
- package/src/client/panel/ProgressHomeView.tsx +0 -87
- package/src/client/panel/PropLibrary.tsx +0 -175
- package/src/client/panel/ReaderView.tsx +0 -306
- package/src/client/panel/ReasoningSection.tsx +0 -31
- package/src/client/panel/RunPanel.tsx +0 -204
- package/src/client/panel/SceneLibrary.tsx +0 -453
- package/src/client/panel/SettingsView.tsx +0 -289
- package/src/client/panel/ShelfView.tsx +0 -242
- 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/client/panel/WorldTab.tsx +0 -202
- package/src/client/panel/controller.ts +0 -45
- package/src/client/panel/helpers.ts +0 -67
- package/src/client/panel/panel.module.css +0 -4359
- package/src/client/panel/views.tsx +0 -314
- package/src/client/sidebar-entry.ts +0 -122
- package/src/client/text.ts +0 -28
- package/src/comic-presets.ts +0 -67
- package/src/docx.ts +0 -83
- package/src/engine.ts +0 -5636
- package/src/global-assets.ts +0 -59
- package/src/index.ts +0 -346
- package/src/llm-live.ts +0 -68
- package/src/manga-genre-rules.ts +0 -87
- package/src/market-radar-scan.ts +0 -41
- package/src/market-radar-sources.ts +0 -173
- package/src/novel-context.ts +0 -287
- package/src/prompt-utils.ts +0 -73
- package/src/prompting.ts +0 -159
- package/src/protocol.ts +0 -2565
- package/src/review-policy.ts +0 -161
- package/src/routes.ts +0 -3939
- package/src/run.ts +0 -338
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 纯展示组件(views):所有状态与事件处理器都留在 NovelPanel,
|
|
3
|
-
* 这里只接收 props 渲染。便于单独维护与复用。
|
|
4
|
-
*/
|
|
5
|
-
import type { ReactElement } from 'react'
|
|
6
|
-
import type { AuditIssue, Plotline, PlotlineHealthReport, PlotlinePlan, RoleRecord, RoleStatusCard } from '../../protocol.ts'
|
|
7
|
-
import { tt, ROLE_LABELS, roleColor, kindLabel, plotlineStatusLabel, plotlineStatusColor } from './helpers.ts'
|
|
8
|
-
import css from './panel.module.css'
|
|
9
|
-
|
|
10
|
-
/** 统计格:状态摘要条 / 资产健康通用。 */
|
|
11
|
-
export function StatCell(props: {
|
|
12
|
-
label: string
|
|
13
|
-
value: string
|
|
14
|
-
detail: string
|
|
15
|
-
/** 值颜色(可选)。 */
|
|
16
|
-
valueColor?: string
|
|
17
|
-
/** 值字号覆盖(如长文本用 13)。 */
|
|
18
|
-
valueFontSize?: number
|
|
19
|
-
/** detail 悬浮提示(可选)。 */
|
|
20
|
-
detailTitle?: string
|
|
21
|
-
}): ReactElement {
|
|
22
|
-
return (
|
|
23
|
-
<div className={css.assetStat}>
|
|
24
|
-
<span className={css.assetStatLabel}>{props.label}</span>
|
|
25
|
-
<span className={css.assetStatValue} style={{ color: props.valueColor, fontSize: props.valueFontSize }}>
|
|
26
|
-
{props.value}
|
|
27
|
-
</span>
|
|
28
|
-
<span className={css.assetStatDetail} title={props.detailTitle}>{props.detail}</span>
|
|
29
|
-
</div>
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** 待办队列行。 */
|
|
34
|
-
export function TodoRow(props: {
|
|
35
|
-
tone: 'danger' | 'warning' | 'info' | 'success'
|
|
36
|
-
title: string
|
|
37
|
-
description: string
|
|
38
|
-
actionLabel: string
|
|
39
|
-
disabled: boolean
|
|
40
|
-
onAction: () => void
|
|
41
|
-
}): ReactElement {
|
|
42
|
-
return (
|
|
43
|
-
<div className={`${css.todoItem} ${props.tone === 'danger' ? css.todoDanger : props.tone === 'warning' ? css.todoWarning : css.todoInfo}`}>
|
|
44
|
-
<span className={css.todoText}>
|
|
45
|
-
{props.title}
|
|
46
|
-
{props.description !== '' && <span className={css.meta}> — {props.description}</span>}
|
|
47
|
-
</span>
|
|
48
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={props.disabled} onClick={props.onAction}>
|
|
49
|
-
{props.actionLabel}
|
|
50
|
-
</button>
|
|
51
|
-
</div>
|
|
52
|
-
)
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** 全书质检问题行。 */
|
|
56
|
-
export function AuditIssueRow(props: {
|
|
57
|
-
issue: AuditIssue
|
|
58
|
-
disabled: boolean
|
|
59
|
-
onFix: () => void
|
|
60
|
-
}): ReactElement {
|
|
61
|
-
const { issue } = props
|
|
62
|
-
return (
|
|
63
|
-
<div className={`${css.todoItem} ${issue.severity === 'high' ? css.todoDanger : issue.severity === 'medium' ? css.todoWarning : css.todoInfo}`}>
|
|
64
|
-
<span className={css.todoText}>
|
|
65
|
-
<span>
|
|
66
|
-
{issue.chapterNo > 0 ? `第 ${issue.chapterNo} 章` : '未定位章节'} · [{issue.severity}] {issue.item}
|
|
67
|
-
</span>
|
|
68
|
-
{issue.suggestion !== '' && <span className={css.meta}>建议:{issue.suggestion}</span>}
|
|
69
|
-
</span>
|
|
70
|
-
{issue.chapterNo > 0 && (
|
|
71
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={props.disabled} onClick={props.onFix}>
|
|
72
|
-
去修订
|
|
73
|
-
</button>
|
|
74
|
-
)}
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/** 剧情线卡片(列表主体)。 */
|
|
80
|
-
export function PlotlineCard(props: {
|
|
81
|
-
line: Plotline
|
|
82
|
-
disabled: boolean
|
|
83
|
-
onRefresh: () => void
|
|
84
|
-
onEdit: () => void
|
|
85
|
-
onRemove: () => void
|
|
86
|
-
}): ReactElement {
|
|
87
|
-
const { line } = props
|
|
88
|
-
return (
|
|
89
|
-
<div key={line.id} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-10)', padding: 'var(--nf-space-8) var(--nf-space-12)', fontSize: 'var(--nf-fs-12)', display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)' }}>
|
|
90
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
91
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
|
|
92
|
-
<b>{line.name}</b>
|
|
93
|
-
<span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{kindLabel(line.kind)}</span>
|
|
94
|
-
<span className={css.badge} style={{ borderColor: plotlineStatusColor(line.status), color: plotlineStatusColor(line.status) }}>{plotlineStatusLabel(line.status)}</span>
|
|
95
|
-
</span>
|
|
96
|
-
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
97
|
-
<button
|
|
98
|
-
type="button"
|
|
99
|
-
className={`${css.button} ${css.buttonSmall}`}
|
|
100
|
-
disabled={props.disabled}
|
|
101
|
-
onClick={props.onRefresh}
|
|
102
|
-
title="AI 结合编年录与章节摘要,自动更新这条线的当前进度"
|
|
103
|
-
>
|
|
104
|
-
↻ AI 刷新进度
|
|
105
|
-
</button>
|
|
106
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={props.disabled} onClick={props.onEdit}>
|
|
107
|
-
{tt('plotlines.edit')}
|
|
108
|
-
</button>
|
|
109
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={props.disabled} onClick={props.onRemove}>
|
|
110
|
-
{tt('plotlines.remove')}
|
|
111
|
-
</button>
|
|
112
|
-
</span>
|
|
113
|
-
</div>
|
|
114
|
-
{line.goal !== '' && <div className={css.meta}><b>{tt('plotlines.goal')}:</b>{line.goal}</div>}
|
|
115
|
-
{line.progress !== '' && <div className={css.meta}><b>{tt('plotlines.progress')}:</b>{line.progress}</div>}
|
|
116
|
-
<div className={css.meta}>
|
|
117
|
-
{tt('plotlines.chapters')}:{line.chapters.length > 0 ? line.chapters.map(n => `第${n}章`).join('、') : '—'}
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
)
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
/** AI 候选角色行(提炼结果,可采纳/修改后采纳)。 */
|
|
124
|
-
export function RoleCandidateRow(props: {
|
|
125
|
-
candidate: RoleRecord
|
|
126
|
-
disabled: boolean
|
|
127
|
-
onAdopt: () => void
|
|
128
|
-
onEdit: () => void
|
|
129
|
-
}): ReactElement {
|
|
130
|
-
const { candidate: r } = props
|
|
131
|
-
const label = ROLE_LABELS[r.roleLabel] ?? r.roleLabel
|
|
132
|
-
const color = roleColor(r.roleLabel)
|
|
133
|
-
return (
|
|
134
|
-
<div key={r.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
135
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
136
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
|
|
137
|
-
<b>{r.name}</b>
|
|
138
|
-
<span className={css.badge} style={{ borderColor: color, color }}>{label}</span>
|
|
139
|
-
{r.identity !== '' && <span className={css.meta}>{r.identity}</span>}
|
|
140
|
-
</span>
|
|
141
|
-
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
142
|
-
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={props.disabled} onClick={props.onAdopt}>
|
|
143
|
-
+ 采纳
|
|
144
|
-
</button>
|
|
145
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={props.disabled} onClick={props.onEdit} title="修改后再采纳(候选列表保留)">
|
|
146
|
-
✏️ 修改后采纳
|
|
147
|
-
</button>
|
|
148
|
-
</span>
|
|
149
|
-
</div>
|
|
150
|
-
{r.goals !== '' && <div className={css.meta}>目标:{r.goals}</div>}
|
|
151
|
-
{r.relations.length > 0 && <div className={css.meta}>关系:{r.relations.join('、')}</div>}
|
|
152
|
-
</div>
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** 已收录角色卡(含从编年录刷新的当前状态行)。 */
|
|
157
|
-
export function RoleCard(props: {
|
|
158
|
-
role: RoleRecord
|
|
159
|
-
status?: RoleStatusCard
|
|
160
|
-
disabled: boolean
|
|
161
|
-
onEdit: () => void
|
|
162
|
-
onRemove: () => void
|
|
163
|
-
}): ReactElement {
|
|
164
|
-
const { role: r, status: st } = props
|
|
165
|
-
const label = ROLE_LABELS[r.roleLabel] ?? r.roleLabel
|
|
166
|
-
const color = roleColor(r.roleLabel)
|
|
167
|
-
return (
|
|
168
|
-
<div key={r.name} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
169
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
170
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
|
|
171
|
-
<b>{r.name}</b>
|
|
172
|
-
<span className={css.badge} style={{ borderColor: color, color }}>{label}</span>
|
|
173
|
-
{r.identity !== '' && <span className={css.meta}>{r.identity}</span>}
|
|
174
|
-
</span>
|
|
175
|
-
<span style={{ display: 'flex', gap: 'var(--nf-space-6)' }}>
|
|
176
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={props.disabled} onClick={props.onEdit}>编辑</button>
|
|
177
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} disabled={props.disabled} onClick={props.onRemove}>删除</button>
|
|
178
|
-
</span>
|
|
179
|
-
</div>
|
|
180
|
-
{r.goals !== '' && <div className={css.meta}>目标:{r.goals}</div>}
|
|
181
|
-
{st !== undefined && (
|
|
182
|
-
<div className={css.meta} style={{ color: 'var(--nf-accent)' }}>
|
|
183
|
-
<b>当前状态:</b>{st.status !== '' ? st.status : '(编年录暂无该角色记录)'}
|
|
184
|
-
{' · 出场 '}{st.appearances} 次 · 最近 第 {st.lastChapter} 章
|
|
185
|
-
</div>
|
|
186
|
-
)}
|
|
187
|
-
{r.relations.length > 0 && <div className={css.meta}>关系:{r.relations.join('、')}</div>}
|
|
188
|
-
{r.arc.length > 0 && <div className={css.meta}>成长线:{r.arc.join(' → ')}</div>}
|
|
189
|
-
{r.knowledge.length > 0 && <div className={css.meta}>知情度:{r.knowledge.join(';')}</div>}
|
|
190
|
-
</div>
|
|
191
|
-
)
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/** 🩺 剧情健康检查报告面板。 */
|
|
195
|
-
export function PlotlineHealthPanel(props: {
|
|
196
|
-
report: PlotlineHealthReport
|
|
197
|
-
disabled: boolean
|
|
198
|
-
onPlan: () => void
|
|
199
|
-
onClose: () => void
|
|
200
|
-
}): ReactElement {
|
|
201
|
-
const { report } = props
|
|
202
|
-
return (
|
|
203
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', border: '1px solid var(--nf-info)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-10)' }}>
|
|
204
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
205
|
-
<b>🩺 剧情健康检查</b>
|
|
206
|
-
<span style={{ display: 'flex', gap: 'var(--nf-space-8)' }}>
|
|
207
|
-
<button
|
|
208
|
-
type="button"
|
|
209
|
-
className={`${css.button} ${css.buttonSmall}`}
|
|
210
|
-
disabled={props.disabled}
|
|
211
|
-
onClick={props.onPlan}
|
|
212
|
-
title="基于本次诊断生成下一阶段剧情方案"
|
|
213
|
-
>
|
|
214
|
-
✨ 基于此设计方案
|
|
215
|
-
</button>
|
|
216
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={props.onClose}>收起</button>
|
|
217
|
-
</span>
|
|
218
|
-
</div>
|
|
219
|
-
<div style={{ fontSize: 'var(--nf-fs-12)', fontWeight: 700, color: 'var(--nf-accent)' }}>判定:{report.verdict}</div>
|
|
220
|
-
{report.timing !== '' && <div className={css.meta}><b>建议时机:</b>{report.timing}</div>}
|
|
221
|
-
{report.reasons.length > 0 && (
|
|
222
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-2)', fontSize: 'var(--nf-fs-12)' }}>
|
|
223
|
-
{report.reasons.map((r, i) => <div key={i} className={css.meta}>· {r}</div>)}
|
|
224
|
-
</div>
|
|
225
|
-
)}
|
|
226
|
-
{report.lines.length > 0 && (
|
|
227
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', fontSize: 'var(--nf-fs-12)' }}>
|
|
228
|
-
{report.lines.map((l, i) => {
|
|
229
|
-
const color = l.health === 'ok' ? 'var(--nf-success)' : l.health === 'warning' ? 'var(--nf-warn)' : 'var(--nf-error)'
|
|
230
|
-
const label = l.health === 'ok' ? '健康' : l.health === 'warning' ? '预警' : '搁置过久'
|
|
231
|
-
return (
|
|
232
|
-
<div key={i} style={{ display: 'flex', gap: 'var(--nf-space-8)', alignItems: 'flex-start' }}>
|
|
233
|
-
<span className={css.badge} style={{ borderColor: color, color, flex: 'none', marginTop: 'var(--nf-space-2)' }}>{label}</span>
|
|
234
|
-
<span className={css.meta}><b>{l.name}</b>:{l.note}</span>
|
|
235
|
-
</div>
|
|
236
|
-
)
|
|
237
|
-
})}
|
|
238
|
-
</div>
|
|
239
|
-
)}
|
|
240
|
-
</div>
|
|
241
|
-
)
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
/** ✨ AI 剧情方案面板。 */
|
|
245
|
-
export function PlotlinePlanPanel(props: {
|
|
246
|
-
plan: PlotlinePlan
|
|
247
|
-
disabled: boolean
|
|
248
|
-
onAdopt: (suggestion: Plotline) => void
|
|
249
|
-
onClose: () => void
|
|
250
|
-
}): ReactElement {
|
|
251
|
-
const { plan } = props
|
|
252
|
-
return (
|
|
253
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', border: '1px solid var(--nf-accent)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-10)' }}>
|
|
254
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
255
|
-
<b>✨ AI 剧情方案</b>
|
|
256
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={props.onClose}>收起</button>
|
|
257
|
-
</div>
|
|
258
|
-
{plan.direction !== '' && (
|
|
259
|
-
<div className={css.meta} style={{ fontSize: 'var(--nf-fs-12)' }}><b>下一阶段方向:</b>{plan.direction}</div>
|
|
260
|
-
)}
|
|
261
|
-
{plan.suggestions.length > 0 && (
|
|
262
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-4)', fontSize: 'var(--nf-fs-12)' }}>
|
|
263
|
-
{plan.suggestions.map((s, i) => (
|
|
264
|
-
<div key={i} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', padding: 'var(--nf-space-6) var(--nf-space-10)' }}>
|
|
265
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
266
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
|
|
267
|
-
<b>{s.name}</b>
|
|
268
|
-
<span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{kindLabel(s.kind)}</span>
|
|
269
|
-
</span>
|
|
270
|
-
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={props.disabled} onClick={() => { props.onAdopt(s) }}>
|
|
271
|
-
+ 采纳
|
|
272
|
-
</button>
|
|
273
|
-
</div>
|
|
274
|
-
{s.goal !== '' && <div className={css.meta}>{s.goal}</div>}
|
|
275
|
-
{s.progress !== '' && <div className={css.meta}>初始进度:{s.progress}</div>}
|
|
276
|
-
</div>
|
|
277
|
-
))}
|
|
278
|
-
</div>
|
|
279
|
-
)}
|
|
280
|
-
</div>
|
|
281
|
-
)
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** ✨ AI 建议剧情线面板。 */
|
|
285
|
-
export function PlotlineSuggestionPanel(props: {
|
|
286
|
-
suggestions: Plotline[]
|
|
287
|
-
disabled: boolean
|
|
288
|
-
onAdopt: (suggestion: Plotline) => void
|
|
289
|
-
onClose: () => void
|
|
290
|
-
}): ReactElement {
|
|
291
|
-
return (
|
|
292
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 'var(--nf-space-6)', border: '1px solid var(--nf-info)', borderRadius: 'var(--nf-radius-12)', padding: 'var(--nf-space-10)' }}>
|
|
293
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
294
|
-
<b>✨ AI 建议({props.suggestions.length} 条)</b>
|
|
295
|
-
<button type="button" className={`${css.button} ${css.buttonSmall}`} onClick={props.onClose}>收起</button>
|
|
296
|
-
</div>
|
|
297
|
-
{props.suggestions.length === 0 && <span className={css.meta}>没有候选线。</span>}
|
|
298
|
-
{props.suggestions.map((s, i) => (
|
|
299
|
-
<div key={i} style={{ border: '1px solid var(--nf-border)', borderRadius: 'var(--nf-radius-8)', padding: 'var(--nf-space-6) var(--nf-space-10)', fontSize: 'var(--nf-fs-12)' }}>
|
|
300
|
-
<div className={css.row} style={{ justifyContent: 'space-between', flexWrap: 'wrap' }}>
|
|
301
|
-
<span style={{ display: 'flex', alignItems: 'center', gap: 'var(--nf-space-6)', flexWrap: 'wrap' }}>
|
|
302
|
-
<b>{s.name}</b>
|
|
303
|
-
<span className={css.badge} style={{ borderColor: 'var(--nf-accent)', color: 'var(--nf-accent)' }}>{kindLabel(s.kind)}</span>
|
|
304
|
-
</span>
|
|
305
|
-
<button type="button" className={`${css.button} ${css.buttonSmall} ${css.buttonPrimary}`} disabled={props.disabled} onClick={() => { props.onAdopt(s) }}>
|
|
306
|
-
+ 采纳
|
|
307
|
-
</button>
|
|
308
|
-
</div>
|
|
309
|
-
{s.goal !== '' && <div className={css.meta}>{s.goal}</div>}
|
|
310
|
-
</div>
|
|
311
|
-
))}
|
|
312
|
-
</div>
|
|
313
|
-
)
|
|
314
|
-
}
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar entry injection — mirrors the family plugins' DOM-level pattern:
|
|
3
|
-
* the shell exposes no external slot, so the entry row is injected after the
|
|
4
|
-
* sibling plugin entries and self-heals via MutationObserver.
|
|
5
|
-
*/
|
|
6
|
-
import type { PanelController } from './panel/controller.ts'
|
|
7
|
-
import { tt } from './panel/helpers.ts'
|
|
8
|
-
import css from './panel/panel.module.css'
|
|
9
|
-
|
|
10
|
-
/** Stable data attribute identifying the injected entry row. */
|
|
11
|
-
export const ENTRY_SELECTOR = '[data-dsh-novelforge-entry]'
|
|
12
|
-
|
|
13
|
-
/** Inline icon: an open book / writing glyph. */
|
|
14
|
-
const ICON = '<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.67 19a2 2 0 0 0 1.416-.588l6.154-6.172a6 6 0 0 0-8.49-8.49L5.586 9.914A2 2 0 0 0 5 11.328V18a1 1 0 0 0 1 1z"/><path d="M16 8 2 22"/><path d="M17.5 15H9"/></svg>'
|
|
15
|
-
|
|
16
|
-
/** Find the sidebar shell root element. */
|
|
17
|
-
function sidebarRoot(): HTMLElement | undefined {
|
|
18
|
-
const column = document.querySelector<HTMLElement>('[data-pane="sidebar"], [class*="sidebarCol"]')
|
|
19
|
-
if (column === null) return undefined
|
|
20
|
-
const logoOwner = column.querySelector<HTMLElement>('[class*="logoRow"]')?.parentElement
|
|
21
|
-
return logoOwner ?? (column.firstElementChild as HTMLElement | undefined)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** The New Session button (nested in the logo row on current shells). */
|
|
25
|
-
function newSessionButton(root: HTMLElement): HTMLButtonElement | undefined {
|
|
26
|
-
const nested = root.querySelector<HTMLButtonElement>('button[class*="newSession"]')
|
|
27
|
-
if (nested !== null) return nested
|
|
28
|
-
for (const child of root.children) {
|
|
29
|
-
if (child.tagName === 'BUTTON') return child as HTMLButtonElement
|
|
30
|
-
}
|
|
31
|
-
return undefined
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Build the entry row (a detached button; insert once the shell is up). */
|
|
35
|
-
function createEntry(controller: PanelController): HTMLButtonElement {
|
|
36
|
-
const entry = document.createElement('button')
|
|
37
|
-
entry.type = 'button'
|
|
38
|
-
entry.dataset.dshNovelforgeEntry = 'true'
|
|
39
|
-
entry.className = css.entry
|
|
40
|
-
entry.setAttribute('aria-label', tt('entry.label'))
|
|
41
|
-
entry.setAttribute('title', tt('entry.tooltip'))
|
|
42
|
-
entry.innerHTML = '<span class="' + css.entryIcon + '">' + ICON + '</span><span class="' + css.entryLabel + '">' + tt('entry.label') + '</span>'
|
|
43
|
-
entry.addEventListener('click', () => { controller.toggle() })
|
|
44
|
-
return entry
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Re-insert the entry after the sibling plugin entry block. */
|
|
48
|
-
function placeEntry(root: HTMLElement, entry: HTMLButtonElement): boolean {
|
|
49
|
-
const button = newSessionButton(root)
|
|
50
|
-
if (button === undefined) return false
|
|
51
|
-
if (entry.parentElement !== root) {
|
|
52
|
-
const row = button.closest('[class*="logoRow"]')
|
|
53
|
-
const base = (row !== null && row.parentElement === root) ? row : button
|
|
54
|
-
const family = Array.from(root.children).filter(
|
|
55
|
-
(el): el is HTMLElement => el instanceof HTMLElement && el.matches('[data-dsh-taskboard-entry], [data-dsh-ssh-entry], [data-dsh-novelforge-entry]'),
|
|
56
|
-
)
|
|
57
|
-
const last = family.length > 0 ? family[family.length - 1] : undefined
|
|
58
|
-
const anchor = last !== undefined ? last.nextElementSibling : base.nextElementSibling
|
|
59
|
-
root.insertBefore(entry, anchor)
|
|
60
|
-
}
|
|
61
|
-
return true
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Mount the sidebar entry, waiting for the shell and self-healing on
|
|
66
|
-
* re-renders.
|
|
67
|
-
*/
|
|
68
|
-
export function mountSidebarEntry(controller: PanelController): () => void {
|
|
69
|
-
const entry = createEntry(controller)
|
|
70
|
-
let root: HTMLElement | undefined
|
|
71
|
-
let placed = false
|
|
72
|
-
|
|
73
|
-
const tryPlace = (): void => {
|
|
74
|
-
if (root !== undefined && !root.isConnected) {
|
|
75
|
-
rootObserver.disconnect()
|
|
76
|
-
root = undefined
|
|
77
|
-
placed = false
|
|
78
|
-
}
|
|
79
|
-
if (placed) {
|
|
80
|
-
if (document.body.contains(entry)) return
|
|
81
|
-
rootObserver.disconnect()
|
|
82
|
-
root = undefined
|
|
83
|
-
placed = false
|
|
84
|
-
}
|
|
85
|
-
root ??= sidebarRoot()
|
|
86
|
-
if (root === undefined) return
|
|
87
|
-
placed = placeEntry(root, entry)
|
|
88
|
-
if (placed) {
|
|
89
|
-
rootObserver.observe(root, { childList: true, subtree: true })
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const waitObserver = new MutationObserver(() => { tryPlace() })
|
|
94
|
-
waitObserver.observe(document.body, { childList: true, subtree: true })
|
|
95
|
-
|
|
96
|
-
const rootObserver = new MutationObserver(() => {
|
|
97
|
-
if (root === undefined || !root.isConnected) {
|
|
98
|
-
placed = false
|
|
99
|
-
tryPlace()
|
|
100
|
-
return
|
|
101
|
-
}
|
|
102
|
-
if (!root.contains(entry)) {
|
|
103
|
-
placed = placeEntry(root, entry)
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
const syncActive = () => {
|
|
108
|
-
if (controller.getSnapshot().panelOpen) entry.dataset.active = 'true'
|
|
109
|
-
else delete entry.dataset.active
|
|
110
|
-
}
|
|
111
|
-
const unsubscribe = controller.subscribe(syncActive)
|
|
112
|
-
syncActive()
|
|
113
|
-
|
|
114
|
-
tryPlace()
|
|
115
|
-
|
|
116
|
-
return () => {
|
|
117
|
-
waitObserver.disconnect()
|
|
118
|
-
rootObserver.disconnect()
|
|
119
|
-
unsubscribe()
|
|
120
|
-
entry.remove()
|
|
121
|
-
}
|
|
122
|
-
}
|
package/src/client/text.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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/comic-presets.ts
DELETED
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
/** 漫画风格预设:每个预设对应一组出图提示词与排版偏好。 */
|
|
2
|
-
|
|
3
|
-
export interface ComicStylePreset {
|
|
4
|
-
id: string
|
|
5
|
-
label: string
|
|
6
|
-
/** 注入 imagePrompt 的风格描述词。 */
|
|
7
|
-
prompt: string
|
|
8
|
-
/** 可选:排版偏好说明。 */
|
|
9
|
-
layoutHint?: string
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export const COMIC_STYLE_PRESETS: ComicStylePreset[] = [
|
|
13
|
-
{
|
|
14
|
-
id: 'japan',
|
|
15
|
-
label: '日漫 / 黑白',
|
|
16
|
-
prompt: 'manga style, screentone, bold ink lines, high contrast, black and white, cel shading',
|
|
17
|
-
layoutHint: '页漫优先',
|
|
18
|
-
},
|
|
19
|
-
{
|
|
20
|
-
id: 'korea',
|
|
21
|
-
label: '韩漫 / 条漫',
|
|
22
|
-
prompt: 'manhwa style, soft cel shading, vibrant colors, clean lineart, webtoon vertical composition, glossy highlights',
|
|
23
|
-
layoutHint: '条漫优先',
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
id: 'china',
|
|
27
|
-
label: '国漫 / 古风',
|
|
28
|
-
prompt: 'chinese ink painting style, flowing brush strokes, traditional palette, ethereal atmosphere, xianxia aesthetic',
|
|
29
|
-
layoutHint: '页漫/条漫均可',
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
id: 'america',
|
|
33
|
-
label: '美漫 / 超级英雄',
|
|
34
|
-
prompt: 'american comic style, bold outlines, dramatic lighting, dynamic poses, vibrant colors, halftone shading',
|
|
35
|
-
layoutHint: '页漫优先',
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
id: 'chibi',
|
|
39
|
-
label: 'Q版 / 搞笑',
|
|
40
|
-
prompt: 'chibi style, cute proportions, big head, exaggerated expressions, simple background, bright colors',
|
|
41
|
-
layoutHint: '条漫/四格优先',
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'realistic',
|
|
45
|
-
label: '写实 / 电影感',
|
|
46
|
-
prompt: 'realistic cinematic style, detailed textures, natural lighting, film grain, high detail',
|
|
47
|
-
layoutHint: '页漫/全彩',
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
id: 'cyber',
|
|
51
|
-
label: '赛博 / 暗黑',
|
|
52
|
-
prompt: 'cyberpunk style, neon lights, dark atmosphere, high contrast, futuristic city, cool tones',
|
|
53
|
-
layoutHint: '页漫/全彩',
|
|
54
|
-
},
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
export function getComicStylePrompt(styleId?: string): string {
|
|
58
|
-
if (!styleId) return ''
|
|
59
|
-
const preset = COMIC_STYLE_PRESETS.find(s => s.id === styleId)
|
|
60
|
-
return preset?.prompt ?? ''
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function getComicStyleLabel(styleId?: string): string {
|
|
64
|
-
if (!styleId) return ''
|
|
65
|
-
const preset = COMIC_STYLE_PRESETS.find(s => s.id === styleId)
|
|
66
|
-
return preset?.label ?? styleId
|
|
67
|
-
}
|
package/src/docx.ts
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* docx outline extraction: a .docx is a zip whose word/document.xml holds the
|
|
3
|
-
* body text in <w:t> runs inside <w:p> paragraphs. We unzip with fflate and
|
|
4
|
-
* walk the XML with a tiny tokenizer — no heavyweight XML/DOM dependency.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { readFileSync } from 'node:fs'
|
|
8
|
-
import { unzipSync, strFromU8 } from 'fflate'
|
|
9
|
-
|
|
10
|
-
/** Decode the handful of XML entities docx bodies actually use. */
|
|
11
|
-
function decodeEntities(text: string): string {
|
|
12
|
-
return text
|
|
13
|
-
.replace(/</g, '<')
|
|
14
|
-
.replace(/>/g, '>')
|
|
15
|
-
.replace(/"/g, '"')
|
|
16
|
-
.replace(/'/g, "'")
|
|
17
|
-
.replace(/&/g, '&')
|
|
18
|
-
.replace(/ /g, ' ')
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Extract plain text from a docx buffer: one line per <w:p> paragraph, with
|
|
23
|
-
* <w:tab>/<w:br> preserved as whitespace. Tables and nested structures are
|
|
24
|
-
* flattened in document order (their paragraphs are just <w:p> too).
|
|
25
|
-
* @param buffer - the raw .docx bytes.
|
|
26
|
-
* @returns the body text.
|
|
27
|
-
*/
|
|
28
|
-
export function extractDocxText(buffer: Uint8Array): string {
|
|
29
|
-
let files: ReturnType<typeof unzipSync>
|
|
30
|
-
try {
|
|
31
|
-
files = unzipSync(buffer)
|
|
32
|
-
} catch (error) {
|
|
33
|
-
throw new Error(`not a valid docx (zip open failed): ${(error as Error).message}`)
|
|
34
|
-
}
|
|
35
|
-
const document = files['word/document.xml']
|
|
36
|
-
if (document === undefined) {
|
|
37
|
-
throw new Error('not a valid docx (word/document.xml missing)')
|
|
38
|
-
}
|
|
39
|
-
const xml = strFromU8(document)
|
|
40
|
-
|
|
41
|
-
const paragraphs: string[] = []
|
|
42
|
-
// Split on paragraph boundaries; keep the segment text of each.
|
|
43
|
-
const parts = xml.split(/<w:p\b[^>]*>/)
|
|
44
|
-
for (let i = 1; i < parts.length; i++) {
|
|
45
|
-
const segment = parts[i]!
|
|
46
|
-
// Runs <w:t ...>…</w:t>; also honor <w:tab/> and <w:br/> as spaces.
|
|
47
|
-
const runs: string[] = []
|
|
48
|
-
const runRe = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>|<w:tab\b[^>]*\/>|<w:br\b[^>]*\/>/g
|
|
49
|
-
for (const match of segment.matchAll(runRe)) {
|
|
50
|
-
if (match[0].startsWith('<w:tab')) {
|
|
51
|
-
runs.push('\t')
|
|
52
|
-
} else if (match[0].startsWith('<w:br')) {
|
|
53
|
-
runs.push('\n')
|
|
54
|
-
} else {
|
|
55
|
-
runs.push(decodeEntities(match[1] ?? ''))
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const line = runs.join('').replace(/\u00a0/g, ' ').trimEnd()
|
|
59
|
-
paragraphs.push(line)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Collapse 3+ blank lines and trim.
|
|
63
|
-
const text = paragraphs.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
64
|
-
if (text.length === 0) {
|
|
65
|
-
throw new Error('docx contains no extractable text')
|
|
66
|
-
}
|
|
67
|
-
return text
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Read and extract a docx outline from disk.
|
|
72
|
-
* @param path - absolute path to the .docx file.
|
|
73
|
-
* @returns the extracted outline text.
|
|
74
|
-
*/
|
|
75
|
-
export function readOutlineFromDocx(path: string): string {
|
|
76
|
-
let buffer: Buffer
|
|
77
|
-
try {
|
|
78
|
-
buffer = readFileSync(path)
|
|
79
|
-
} catch (error) {
|
|
80
|
-
throw new Error(`cannot read outline file "${path}": ${(error as Error).message}`)
|
|
81
|
-
}
|
|
82
|
-
return extractDocxText(new Uint8Array(buffer))
|
|
83
|
-
}
|