@waterwx/dsh-novel-forge 2.0.0-alpha.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -285
- package/lib/client.js +19561 -19439
- package/lib/index.js +8159 -4456
- package/lib/types/action-guard.d.ts +61 -0
- package/lib/types/ai-scan.d.ts +0 -6
- package/lib/types/assets.d.ts +20 -9
- package/lib/types/assistant.d.ts +29 -0
- package/lib/types/author-assets.d.ts +1 -3
- package/lib/types/bookshelf.d.ts +13 -3
- package/lib/types/client/api.d.ts +136 -27
- package/lib/types/client/chip-prefs.d.ts +36 -0
- package/lib/types/client/docx.d.ts +2 -2
- package/lib/types/client/llmLive.d.ts +31 -0
- package/lib/types/client/locales.d.ts +15 -70
- package/lib/types/client/mount.d.ts +0 -2
- package/lib/types/client/panel/AiTag.d.ts +1 -0
- package/lib/types/client/panel/AssistantTab.d.ts +4 -1
- package/lib/types/client/panel/AuthorHome.d.ts +13 -11
- package/lib/types/client/panel/BookAnalysisView.d.ts +4 -0
- package/lib/types/client/panel/CmdPalette.d.ts +12 -0
- package/lib/types/client/panel/CreateBookView.d.ts +5 -1
- package/lib/types/client/panel/DirectorView.d.ts +7 -0
- package/lib/types/client/panel/IdeaInspirationView.d.ts +6 -0
- package/lib/types/client/panel/KnowledgeBaseView.d.ts +4 -0
- package/lib/types/client/panel/LiveFeedLog.d.ts +13 -0
- package/lib/types/client/panel/MarketRadarView.d.ts +5 -0
- package/lib/types/client/panel/ModelManager.d.ts +2 -2
- package/lib/types/client/panel/NovelPanel.d.ts +6 -1
- package/lib/types/client/panel/PageHeader.d.ts +18 -0
- package/lib/types/client/panel/PromptSlotsView.d.ts +11 -0
- package/lib/types/client/panel/SettingsView.d.ts +12 -9
- package/lib/types/client/panel/ShelfView.d.ts +10 -1
- package/lib/types/client/panel/SubPage.d.ts +27 -0
- package/lib/types/client/panel/TensionView.d.ts +18 -0
- package/lib/types/client/panel/TimelineView.d.ts +18 -0
- package/lib/types/client/panel/helpers.d.ts +5 -3
- package/lib/types/client/panel/market-radar-state.d.ts +56 -0
- package/lib/types/client/panel/toast.d.ts +2 -0
- package/lib/types/client/panel/views.d.ts +21 -35
- package/lib/types/client/run-status.d.ts +21 -0
- package/lib/types/client/sidebar-entry.d.ts +0 -2
- package/lib/types/docx-core.d.ts +16 -0
- package/lib/types/docx.d.ts +2 -3
- package/lib/types/engine.d.ts +78 -103
- package/lib/types/global-assets.d.ts +6 -0
- package/lib/types/home.d.ts +11 -0
- package/lib/types/index.d.ts +26 -12
- package/lib/types/llm-live.d.ts +70 -0
- package/lib/types/llm-retry.d.ts +38 -0
- package/lib/types/market-radar-scan.d.ts +18 -0
- package/lib/types/market-radar-sources.d.ts +31 -0
- package/lib/types/migrate-output.d.ts +25 -0
- package/lib/types/model-capability.d.ts +93 -0
- package/lib/types/output-budget.d.ts +86 -0
- package/lib/types/prompt-slots.d.ts +57 -0
- package/lib/types/prompting.d.ts +10 -0
- package/lib/types/protocol.d.ts +816 -601
- package/lib/types/revision.d.ts +76 -0
- package/lib/types/routes.d.ts +9 -3
- package/lib/types/run.d.ts +23 -1
- package/lib/types/settings-persist.d.ts +59 -0
- package/lib/types/snapshots.d.ts +46 -0
- package/lib/types/stage-contract.d.ts +58 -0
- package/lib/types/tension.d.ts +53 -0
- package/lib/types/timeline.d.ts +37 -0
- package/package.json +50 -25
- package/src/action-guard.ts +127 -0
- package/src/ai-scan.ts +0 -24
- package/src/assets.ts +1801 -879
- package/src/assistant.ts +1098 -770
- package/src/author-assets.ts +131 -147
- package/src/bookshelf.ts +193 -167
- package/src/client/api.ts +767 -617
- package/src/client/chip-prefs.ts +121 -0
- package/src/client/docx.ts +32 -69
- package/src/client/index.ts +2 -0
- package/src/client/llmLive.ts +111 -0
- package/src/client/locales.ts +15 -274
- package/src/client/mount.tsx +3 -2
- package/src/client/panel/AdaptModeView.tsx +535 -520
- package/src/client/panel/AiTag.tsx +7 -0
- package/src/client/panel/AssetsTab.tsx +448 -458
- package/src/client/panel/AssistantTab.tsx +355 -301
- package/src/client/panel/AuthorAssetsView.tsx +205 -207
- package/src/client/panel/AuthorHome.tsx +309 -204
- package/src/client/panel/BookAnalysisView.tsx +63 -0
- package/src/client/panel/CmdPalette.tsx +102 -0
- package/src/client/panel/CreateBookView.tsx +366 -352
- package/src/client/panel/DirectorView.tsx +200 -0
- package/src/client/panel/GlobalAssetLibraryView.tsx +168 -172
- package/src/client/panel/IdeaInspirationView.tsx +76 -0
- package/src/client/panel/ImportModal.tsx +302 -302
- package/src/client/panel/KnowledgeBaseView.tsx +125 -0
- package/src/client/panel/LiveFeedLog.tsx +204 -0
- package/src/client/panel/MarketRadarView.tsx +415 -0
- package/src/client/panel/ModelManager.tsx +205 -205
- package/src/client/panel/NovelPanel.tsx +3144 -2205
- package/src/client/panel/PageHeader.tsx +37 -0
- package/src/client/panel/ProgressConsole.tsx +63 -64
- package/src/client/panel/PromptSlotsView.tsx +97 -0
- package/src/client/panel/ReaderView.tsx +306 -306
- package/src/client/panel/RunPanel.tsx +56 -24
- package/src/client/panel/SettingsView.tsx +175 -106
- package/src/client/panel/ShelfView.tsx +63 -39
- package/src/client/panel/SubPage.tsx +42 -0
- package/src/client/panel/TensionView.tsx +212 -0
- package/src/client/panel/TimelineView.tsx +208 -0
- package/src/client/panel/WorldTab.tsx +202 -202
- package/src/client/panel/helpers.ts +7 -5
- package/src/client/panel/market-radar-state.ts +79 -0
- package/src/client/panel/panel.module.css +5767 -4360
- package/src/client/panel/toast.ts +51 -0
- package/src/client/panel/views.tsx +316 -314
- package/src/client/run-status.ts +171 -0
- package/src/client/sidebar-entry.ts +3 -3
- package/src/docx-core.ts +53 -0
- package/src/docx.ts +44 -83
- package/src/engine.ts +1197 -1638
- package/src/global-assets.ts +58 -0
- package/src/home.ts +33 -0
- package/src/index.ts +181 -101
- package/src/llm-live.ts +245 -0
- package/src/llm-retry.ts +77 -0
- package/src/market-radar-scan.ts +35 -0
- package/src/market-radar-sources.ts +173 -0
- package/src/migrate-output.ts +76 -0
- package/src/model-capability.ts +215 -0
- package/src/novel-context.ts +286 -287
- package/src/output-budget.ts +147 -0
- package/src/prompt-slots.ts +125 -0
- package/src/prompting.ts +26 -0
- package/src/protocol.ts +835 -614
- package/src/revision.ts +283 -0
- package/src/routes.ts +1286 -910
- package/src/run.ts +113 -17
- package/src/settings-persist.ts +114 -0
- package/src/snapshots.ts +174 -0
- package/src/stage-contract.ts +186 -0
- package/src/tension.ts +251 -0
- package/src/timeline.ts +159 -0
- package/assets/styles/arcane-thick-3d.webp +0 -0
- package/assets/styles/chinese-ink.webp +0 -0
- package/assets/styles/clay-stopmotion-3d.webp +0 -0
- package/assets/styles/dark-ghibli.webp +0 -0
- package/assets/styles/dark-gothic-3d.webp +0 -0
- package/assets/styles/film-photo.webp +0 -0
- package/assets/styles/guofeng-heavy.webp +0 -0
- package/assets/styles/guoman-2d-gufeng.webp +0 -0
- package/assets/styles/guoman-3d-tianyuan.webp +0 -0
- package/assets/styles/guoman-3d-xuanhuan.webp +0 -0
- package/assets/styles/horror-game-3d.webp +0 -0
- package/assets/styles/hyperreal-3d.webp +0 -0
- package/assets/styles/japan-cel-dark.webp +0 -0
- package/assets/styles/korean-webtoon.webp +0 -0
- package/assets/styles/low-cyberpunk-3d.webp +0 -0
- package/assets/styles/nextgen-cg.webp +0 -0
- package/assets/styles/noir-film.webp +0 -0
- package/assets/styles/openworld-survival.webp +0 -0
- package/assets/styles/paper-cut-3d.webp +0 -0
- package/assets/styles/pixar-adult-3d.webp +0 -0
- package/assets/styles/pop-marvel-2d.webp +0 -0
- package/assets/styles/sandiao-qban.webp +0 -0
- package/assets/styles/shinkai-anime.webp +0 -0
- package/assets/styles/tactical-stealth.webp +0 -0
- package/assets/styles/urban-lightreal-3d.webp +0 -0
- package/assets/styles/vaporwave-3d.webp +0 -0
- package/assets/styles/vhs90.webp +0 -0
- package/lib/types/client/panel/BookshelfBar.d.ts +0 -11
- package/lib/types/client/panel/FlowGuide.d.ts +0 -22
- package/lib/types/client/panel/MangaRoleLibrary.d.ts +0 -20
- package/lib/types/client/panel/MangaWorkspace.d.ts +0 -12
- package/lib/types/client/panel/ModelPicker.d.ts +0 -11
- package/lib/types/client/panel/ProgressHomeView.d.ts +0 -5
- package/lib/types/client/panel/PropLibrary.d.ts +0 -8
- package/lib/types/client/panel/SceneLibrary.d.ts +0 -16
- package/lib/types/client/panel/StoryboardTab.d.ts +0 -19
- package/lib/types/client/panel/StyleCard.d.ts +0 -8
- package/lib/types/client/panel/StyleGallery.d.ts +0 -1
- package/lib/types/comic-presets.d.ts +0 -12
- package/lib/types/manga-genre-rules.d.ts +0 -22
- package/lib/types/prompt-utils.d.ts +0 -35
- package/lib/types/review-policy.d.ts +0 -97
- package/lib/types/shot-language.d.ts +0 -46
- package/lib/types/story-beat-language.d.ts +0 -35
- package/lib/types/style-library.d.ts +0 -33
- package/skills/chapter-batch/SKILL.md +0 -81
- package/src/client/mount.tsx._new +0 -97
- package/src/client/mount.tsx._nf +0 -97
- package/src/client/mount.tsx._tmp +0 -97
- package/src/client/panel/BookshelfBar.tsx +0 -170
- package/src/client/panel/FlowGuide.tsx +0 -84
- package/src/client/panel/MangaRoleLibrary.tsx +0 -1002
- package/src/client/panel/MangaWorkspace.tsx +0 -715
- package/src/client/panel/ProgressHomeView.tsx +0 -87
- package/src/client/panel/PropLibrary.tsx +0 -175
- package/src/client/panel/SceneLibrary.tsx +0 -453
- package/src/client/panel/StoryboardTab.tsx +0 -472
- package/src/client/panel/StyleCard.tsx +0 -90
- package/src/client/panel/StyleGallery.tsx +0 -58
- package/src/comic-presets.ts +0 -67
- package/src/manga-genre-rules.ts +0 -87
- package/src/prompt-utils.ts +0 -73
- package/src/review-policy.ts +0 -161
- package/src/shot-language.ts +0 -181
- package/src/story-beat-language.ts +0 -130
- package/src/style-library.ts +0 -84
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 右上角「小说工坊生成状态」芯片的显示偏好(纯客户端、localStorage)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是 localStorage 而不是插件配置:这是纯界面 chrome,与显示模式 / 界面密度 /
|
|
5
|
+
* 编辑器字号 / 芯片自身的位置记忆同类;哪个浏览器想清屏就哪个浏览器清,不需要跨端同步。
|
|
6
|
+
* 将来若要跨端,把键名沿用成 config 字段 `showRunChip` 即可平滑升级。
|
|
7
|
+
*
|
|
8
|
+
* 三态:
|
|
9
|
+
* off 关闭(同时停止 4 秒轮询,不白跑)
|
|
10
|
+
* active 只在生产单**运行中或已暂停**时显示(不想错过生成,也不想留残影)
|
|
11
|
+
* always 只要该目录有生产单状态就显示(默认,保持历史行为)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { RunState } from '../protocol.ts'
|
|
15
|
+
|
|
16
|
+
export type RunChipMode = 'off' | 'active' | 'always'
|
|
17
|
+
|
|
18
|
+
/** localStorage 键(与芯片位置记忆同一命名空间)。 */
|
|
19
|
+
export const RUN_CHIP_MODE_KEY = 'dsh-novel-forge.runChip.mode'
|
|
20
|
+
|
|
21
|
+
/** 同页变更通知用的自定义事件名(跨标签页走原生 storage 事件)。 */
|
|
22
|
+
export const RUN_CHIP_MODE_EVENT = 'dsh-novel-forge:runchip-mode'
|
|
23
|
+
|
|
24
|
+
/** 默认值与历史行为一致:有生产单状态就显示。 */
|
|
25
|
+
export const DEFAULT_RUN_CHIP_MODE: RunChipMode = 'always'
|
|
26
|
+
|
|
27
|
+
/** 设置页下拉用的三态选项(顺序即用户看到的顺序)。 */
|
|
28
|
+
export const RUN_CHIP_MODE_OPTIONS: ReadonlyArray<{ value: RunChipMode; label: string }> = [
|
|
29
|
+
{ value: 'always', label: '总是显示' },
|
|
30
|
+
{ value: 'active', label: '仅生成中 / 暂停时显示' },
|
|
31
|
+
{ value: 'off', label: '关闭' },
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
const ALL_MODES: readonly RunChipMode[] = ['off', 'active', 'always']
|
|
35
|
+
|
|
36
|
+
function isMode(value: unknown): value is RunChipMode {
|
|
37
|
+
return typeof value === 'string' && (ALL_MODES as readonly string[]).includes(value)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 安全拿 localStorage:隐私模式 / 非浏览器环境下为 undefined。 */
|
|
41
|
+
function storage(): Storage | undefined {
|
|
42
|
+
try {
|
|
43
|
+
return typeof localStorage === 'undefined' ? undefined : localStorage
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** 读取偏好(缺失 / 非法 / 存储不可用 → 默认值)。 */
|
|
50
|
+
export function readRunChipMode(): RunChipMode {
|
|
51
|
+
try {
|
|
52
|
+
const raw = storage()?.getItem(RUN_CHIP_MODE_KEY)
|
|
53
|
+
return isMode(raw) ? raw : DEFAULT_RUN_CHIP_MODE
|
|
54
|
+
} catch {
|
|
55
|
+
return DEFAULT_RUN_CHIP_MODE
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** 已注册的同页监听者(storage 事件只覆盖跨标签页,同页要靠这里)。 */
|
|
60
|
+
const listeners = new Set<(mode: RunChipMode) => void>()
|
|
61
|
+
|
|
62
|
+
/** 写入偏好并通知所有监听者;存储不可用时仍通知(本次会话内生效)。 */
|
|
63
|
+
export function writeRunChipMode(mode: RunChipMode): void {
|
|
64
|
+
try {
|
|
65
|
+
storage()?.setItem(RUN_CHIP_MODE_KEY, mode)
|
|
66
|
+
} catch {
|
|
67
|
+
/* 存储不可用:本次会话内仍然生效 */
|
|
68
|
+
}
|
|
69
|
+
for (const listener of [...listeners]) {
|
|
70
|
+
try {
|
|
71
|
+
listener(mode)
|
|
72
|
+
} catch {
|
|
73
|
+
/* 单个监听者出错不影响其他人 */
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
if (typeof window !== 'undefined' && typeof CustomEvent === 'function') {
|
|
78
|
+
window.dispatchEvent(new CustomEvent(RUN_CHIP_MODE_EVENT, { detail: mode }))
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
/* 事件派发失败无关紧要 */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** 订阅偏好变化(同页 + 跨标签页 storage);返回退订函数。 */
|
|
86
|
+
export function subscribeRunChipMode(listener: (mode: RunChipMode) => void): () => void {
|
|
87
|
+
listeners.add(listener)
|
|
88
|
+
const onStorage = (event: StorageEvent): void => {
|
|
89
|
+
if (event.key !== null && event.key !== RUN_CHIP_MODE_KEY) return
|
|
90
|
+
listener(readRunChipMode())
|
|
91
|
+
}
|
|
92
|
+
let attached = false
|
|
93
|
+
try {
|
|
94
|
+
if (typeof window !== 'undefined') {
|
|
95
|
+
window.addEventListener('storage', onStorage)
|
|
96
|
+
attached = true
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
attached = false
|
|
100
|
+
}
|
|
101
|
+
return () => {
|
|
102
|
+
listeners.delete(listener)
|
|
103
|
+
if (!attached) return
|
|
104
|
+
try {
|
|
105
|
+
window.removeEventListener('storage', onStorage)
|
|
106
|
+
} catch {
|
|
107
|
+
/* ignore */
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 给定偏好与当前生产单状态,判断芯片是否应可见。
|
|
114
|
+
* null(没有生产单状态)永远不显示——这是渲染层与测试共用的唯一判据。
|
|
115
|
+
*/
|
|
116
|
+
export function isRunChipVisible(mode: RunChipMode, state: RunState | null | undefined): boolean {
|
|
117
|
+
if (state === null || state === undefined) return false
|
|
118
|
+
if (mode === 'off') return false
|
|
119
|
+
if (mode === 'active') return state.status === 'running' || state.status === 'paused'
|
|
120
|
+
return true
|
|
121
|
+
}
|
package/src/client/docx.ts
CHANGED
|
@@ -1,69 +1,32 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Browser-side docx outline extraction: a .docx is a zip whose
|
|
3
|
-
* word/document.xml holds the body text in <w:t> runs inside <w:p> paragraphs.
|
|
4
|
-
* Uses fflate (inlined into the client bundle) so the user can pick or drag a
|
|
5
|
-
* docx without any server upload.
|
|
6
|
-
*
|
|
7
|
-
* Import from 'fflate/browser' (not 'fflate'): the default entry resolves to
|
|
8
|
-
* the Node build (esm/index.mjs), which calls module.createRequire() for the
|
|
9
|
-
* optional worker_threads path — inlining that into the browser bundle leaves
|
|
10
|
-
* a bare require("module") the client-modules table cannot answer.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new Error(`不是有效的 docx(zip 解压失败):${(error as Error).message}`)
|
|
34
|
-
}
|
|
35
|
-
const document = files['word/document.xml']
|
|
36
|
-
if (document === undefined) {
|
|
37
|
-
throw new Error('不是有效的 docx(缺少 word/document.xml)')
|
|
38
|
-
}
|
|
39
|
-
const xml = strFromU8(document)
|
|
40
|
-
|
|
41
|
-
const paragraphs: string[] = []
|
|
42
|
-
const parts = xml.split(/<w:p\b[^>]*>/)
|
|
43
|
-
for (let i = 1; i < parts.length; i++) {
|
|
44
|
-
const segment = parts[i]!
|
|
45
|
-
const runs: string[] = []
|
|
46
|
-
const runRe = /<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>|<w:tab\b[^>]*\/>|<w:br\b[^>]*\/>/g
|
|
47
|
-
for (const match of segment.matchAll(runRe)) {
|
|
48
|
-
if (match[0].startsWith('<w:tab')) {
|
|
49
|
-
runs.push('\t')
|
|
50
|
-
} else if (match[0].startsWith('<w:br')) {
|
|
51
|
-
runs.push('\n')
|
|
52
|
-
} else {
|
|
53
|
-
runs.push(decodeEntities(match[1] ?? ''))
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
paragraphs.push(runs.join('').replace(/\u00a0/g, ' ').trimEnd())
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const text = paragraphs.join('\n').replace(/\n{3,}/g, '\n\n').trim()
|
|
60
|
-
if (text.length === 0) {
|
|
61
|
-
throw new Error('docx 中没有可提取的文本')
|
|
62
|
-
}
|
|
63
|
-
return text
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Read a File as ArrayBuffer. */
|
|
67
|
-
export function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
|
68
|
-
return file.arrayBuffer()
|
|
69
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Browser-side docx outline extraction: a .docx is a zip whose
|
|
3
|
+
* word/document.xml holds the body text in <w:t> runs inside <w:p> paragraphs.
|
|
4
|
+
* Uses fflate (inlined into the client bundle) so the user can pick or drag a
|
|
5
|
+
* docx without any server upload.
|
|
6
|
+
*
|
|
7
|
+
* Import from 'fflate/browser' (not 'fflate'): the default entry resolves to
|
|
8
|
+
* the Node build (esm/index.mjs), which calls module.createRequire() for the
|
|
9
|
+
* optional worker_threads path — inlining that into the browser bundle leaves
|
|
10
|
+
* a bare require("module") the client-modules table cannot answer.
|
|
11
|
+
*
|
|
12
|
+
* XML 解析共享核心在 ../docx-core.ts(与服务端同源)。
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { unzipSync, strFromU8 } from 'fflate/browser'
|
|
16
|
+
import { extractDocxTextFromXml } from '../docx-core.ts'
|
|
17
|
+
|
|
18
|
+
/** Extract plain text from a docx buffer: one line per <w:p> paragraph. */
|
|
19
|
+
export function extractDocxTextFromBuffer(buffer: ArrayBuffer | Uint8Array): string {
|
|
20
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer)
|
|
21
|
+
let files: ReturnType<typeof unzipSync>
|
|
22
|
+
try {
|
|
23
|
+
files = unzipSync(bytes)
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw new Error(`不是有效的 docx(zip 解压失败):${(error as Error).message}`)
|
|
26
|
+
}
|
|
27
|
+
const document = files['word/document.xml']
|
|
28
|
+
if (document === undefined) {
|
|
29
|
+
throw new Error('不是有效的 docx(缺少 word/document.xml)')
|
|
30
|
+
}
|
|
31
|
+
return extractDocxTextFromXml(strFromU8(document))
|
|
32
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { NovelApi } from './api.ts'
|
|
|
9
9
|
import { mountPanel } from './mount.tsx'
|
|
10
10
|
import { PanelController } from './panel/controller.ts'
|
|
11
11
|
import { mountSidebarEntry } from './sidebar-entry.ts'
|
|
12
|
+
import { mountRunStatus } from './run-status.ts'
|
|
12
13
|
|
|
13
14
|
/** Required services (fiber inject waiting). */
|
|
14
15
|
export const inject = ['slots', 'locale']
|
|
@@ -24,6 +25,7 @@ export function apply(ctx: ClientContext): void {
|
|
|
24
25
|
try {
|
|
25
26
|
disposers.push(mountSidebarEntry(controller))
|
|
26
27
|
disposers.push(mountPanel(controller, api))
|
|
28
|
+
disposers.push(mountRunStatus(controller, api))
|
|
27
29
|
} catch (error) {
|
|
28
30
|
// DOM failures degrade the panel, never the GUI.
|
|
29
31
|
console.warn('[dsh-novel-forge] mount failed:', error)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 浏览器端「AI 创作实况」钩子:连接 /llm-live/stream (SSE),按 LLM 调用分组会话。
|
|
3
|
+
* 轻量实现,不持久化缓存(对齐上游 useLlmLiveFeed 的核心行为)。
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useEffect, useMemo, useState } from 'react'
|
|
7
|
+
import { NOVEL_API, type LlmLiveFrame, type LlmTokenUsage } from '../protocol.ts'
|
|
8
|
+
|
|
9
|
+
// 帧结构定义在 protocol.ts(宿主与浏览器共用同一份线上形状),此处只再导出。
|
|
10
|
+
export type { LlmLiveFrame }
|
|
11
|
+
|
|
12
|
+
export interface LlmLiveSession {
|
|
13
|
+
sessionId: string
|
|
14
|
+
label: string
|
|
15
|
+
model?: string
|
|
16
|
+
phase: 'requesting' | 'streaming' | 'completed' | 'failed'
|
|
17
|
+
phaseMessage: string
|
|
18
|
+
preview: string
|
|
19
|
+
totalChars: number
|
|
20
|
+
startedAt: string
|
|
21
|
+
updatedAt: string
|
|
22
|
+
/** 本次调用的 token 用量(供应商未上报时缺省)。 */
|
|
23
|
+
usage?: LlmTokenUsage
|
|
24
|
+
/** 本次调用耗时(毫秒)。 */
|
|
25
|
+
elapsedMs?: number
|
|
26
|
+
/** 首字耗时(毫秒)。 */
|
|
27
|
+
firstTokenMs?: number
|
|
28
|
+
/** 是否保存了可查看的 Prompt。 */
|
|
29
|
+
hasPrompt?: boolean
|
|
30
|
+
/** Prompt 字符数(system + user)。 */
|
|
31
|
+
promptChars?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const MAX_PREVIEW_CHARS = 20_000
|
|
35
|
+
|
|
36
|
+
function applyFrame(current: Record<string, LlmLiveSession>, frame: LlmLiveFrame): Record<string, LlmLiveSession> {
|
|
37
|
+
const next = { ...current }
|
|
38
|
+
const id = frame.sessionId
|
|
39
|
+
let session = next[id]
|
|
40
|
+
if (frame.type === 'session_started') {
|
|
41
|
+
session = {
|
|
42
|
+
sessionId: id,
|
|
43
|
+
label: frame.label ?? 'LLM 调用',
|
|
44
|
+
model: frame.model,
|
|
45
|
+
phase: 'requesting',
|
|
46
|
+
phaseMessage: '正在连接模型',
|
|
47
|
+
preview: '',
|
|
48
|
+
totalChars: 0,
|
|
49
|
+
startedAt: frame.at,
|
|
50
|
+
updatedAt: frame.at,
|
|
51
|
+
hasPrompt: frame.hasPrompt,
|
|
52
|
+
promptChars: frame.promptChars,
|
|
53
|
+
}
|
|
54
|
+
next[id] = session
|
|
55
|
+
return next
|
|
56
|
+
}
|
|
57
|
+
if (session === undefined) return next
|
|
58
|
+
if (frame.type === 'output_delta') {
|
|
59
|
+
const preview = (session.preview + (frame.content ?? '')).slice(-MAX_PREVIEW_CHARS)
|
|
60
|
+
session = { ...session, phase: session.phase === 'requesting' ? 'streaming' : session.phase, phaseMessage: session.phase === 'requesting' ? '模型正在返回内容' : session.phaseMessage, preview, totalChars: frame.totalChars ?? session.totalChars + (frame.content?.length ?? 0), updatedAt: frame.at }
|
|
61
|
+
next[id] = session
|
|
62
|
+
} else if (frame.type === 'reasoning_delta') {
|
|
63
|
+
session = { ...session, phase: session.phase === 'requesting' ? 'streaming' : session.phase, updatedAt: frame.at }
|
|
64
|
+
next[id] = session
|
|
65
|
+
} else if (frame.type === 'phase_changed') {
|
|
66
|
+
session = { ...session, phase: frame.phase ?? session.phase, phaseMessage: frame.phaseMessage ?? session.phaseMessage, updatedAt: frame.at }
|
|
67
|
+
next[id] = session
|
|
68
|
+
} else if (frame.type === 'session_completed') {
|
|
69
|
+
session = {
|
|
70
|
+
...session,
|
|
71
|
+
phase: frame.phase ?? 'completed',
|
|
72
|
+
phaseMessage: frame.phase === 'failed' ? (frame.error ?? '调用失败') : '模型结果已准备完成',
|
|
73
|
+
totalChars: frame.totalChars ?? session.totalChars,
|
|
74
|
+
preview: session.preview || (frame.preview ?? ''),
|
|
75
|
+
updatedAt: frame.at,
|
|
76
|
+
usage: frame.usage ?? session.usage,
|
|
77
|
+
elapsedMs: frame.elapsedMs ?? session.elapsedMs,
|
|
78
|
+
firstTokenMs: frame.firstTokenMs ?? session.firstTokenMs,
|
|
79
|
+
}
|
|
80
|
+
next[id] = session
|
|
81
|
+
}
|
|
82
|
+
return next
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function useLlmLiveFeed(enabled = true): { connected: boolean; sessions: LlmLiveSession[] } {
|
|
86
|
+
const [sessions, setSessions] = useState<Record<string, LlmLiveSession>>({})
|
|
87
|
+
const [connected, setConnected] = useState(false)
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!enabled) {
|
|
90
|
+
setSessions({})
|
|
91
|
+
setConnected(false)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
const es = new EventSource(NOVEL_API.llmLive)
|
|
95
|
+
es.onopen = () => setConnected(true)
|
|
96
|
+
es.onmessage = (ev) => {
|
|
97
|
+
try {
|
|
98
|
+
const frame = JSON.parse(ev.data as string) as LlmLiveFrame
|
|
99
|
+
setSessions(prev => applyFrame(prev, frame))
|
|
100
|
+
} catch { /* ignore malformed frame */ }
|
|
101
|
+
}
|
|
102
|
+
// EventSource 断线会自动重连;这里只在断线瞬间标记未连接,重连成功后会再次 onopen。
|
|
103
|
+
es.onerror = () => setConnected(false)
|
|
104
|
+
return () => {
|
|
105
|
+
es.close()
|
|
106
|
+
setConnected(false)
|
|
107
|
+
}
|
|
108
|
+
}, [enabled])
|
|
109
|
+
const list = useMemo(() => Object.values(sessions).sort((a, b) => a.startedAt.localeCompare(b.startedAt)), [sessions])
|
|
110
|
+
return { connected, sessions: list }
|
|
111
|
+
}
|