@waterwx/dsh-novel-forge 1.3.1-alpha → 1.3.2-alpha
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 +3 -2
- package/lib/client.js +653 -458
- package/lib/index.js +24 -1
- package/lib/types/client/chip-prefs.d.ts +36 -0
- package/lib/types/client/run-status.d.ts +2 -0
- package/lib/types/protocol.d.ts +6 -0
- package/lib/types/run.d.ts +12 -0
- package/package.json +1 -1
- package/src/client/chip-prefs.ts +121 -0
- package/src/client/panel/RunPanel.tsx +236 -223
- package/src/client/panel/SettingsView.tsx +354 -340
- package/src/client/panel/panel.module.css +5766 -5763
- package/src/client/run-status.ts +171 -143
- package/src/protocol.ts +6 -0
- package/src/run.ts +37 -1
package/lib/index.js
CHANGED
|
@@ -9515,6 +9515,24 @@ function revisionTodoText(item) {
|
|
|
9515
9515
|
function runStateFile(outputDir) {
|
|
9516
9516
|
return join(outputDir, "run-state.json");
|
|
9517
9517
|
}
|
|
9518
|
+
/**
|
|
9519
|
+
* 解析某个输出目录的书名(用于旧 run-state.json 的兜底)。
|
|
9520
|
+
* 优先项目文件里的 bookName,其次目录名——两者都没有时返回空串。
|
|
9521
|
+
* @param outputDir - 该书输出目录。
|
|
9522
|
+
*/
|
|
9523
|
+
function resolveRunBookName(outputDir) {
|
|
9524
|
+
const fromProject = loadProject(outputDir)?.bookName;
|
|
9525
|
+
if (typeof fromProject === "string" && fromProject.trim() !== "") return fromProject.trim();
|
|
9526
|
+
return (outputDir.replace(/[\\/]+$/, "").split(/[\\/]/).pop() ?? "").trim();
|
|
9527
|
+
}
|
|
9528
|
+
/**
|
|
9529
|
+
* 一句话描述"正在跑的是哪本书的哪一章"。
|
|
9530
|
+
* 面板状态行与 409 报错共用,避免两处各写各的(也便于单测覆盖)。
|
|
9531
|
+
* @param state - 运行中的生产单状态。
|
|
9532
|
+
*/
|
|
9533
|
+
function describeRunningBatch(state) {
|
|
9534
|
+
return `${typeof state.bookName === "string" && state.bookName.trim() !== "" ? `《${state.bookName.trim()}》` : "未标注书目"}第${state.currentNo}章`;
|
|
9535
|
+
}
|
|
9518
9536
|
var ProductionRunner = class {
|
|
9519
9537
|
deps;
|
|
9520
9538
|
state = null;
|
|
@@ -9540,6 +9558,7 @@ var ProductionRunner = class {
|
|
|
9540
9558
|
const parsed = JSON.parse(raw);
|
|
9541
9559
|
if (parsed?.runId === void 0) continue;
|
|
9542
9560
|
if (parsed.status === "running") parsed.status = "paused";
|
|
9561
|
+
if (parsed.bookName === void 0 || parsed.bookName === "") parsed.bookName = resolveRunBookName(outputDir);
|
|
9543
9562
|
this.state = parsed;
|
|
9544
9563
|
this.bookDir = outputDir;
|
|
9545
9564
|
return this.state;
|
|
@@ -9565,7 +9584,10 @@ var ProductionRunner = class {
|
|
|
9565
9584
|
/** 启动/续跑生产单:startNo..endNo 区间,endNo 超出计划时先自动补计划。 */
|
|
9566
9585
|
async start(startNo, endNo, runDir) {
|
|
9567
9586
|
const config = this.deps.getConfig();
|
|
9568
|
-
if (this.working)
|
|
9587
|
+
if (this.working) {
|
|
9588
|
+
const running = this.state;
|
|
9589
|
+
throw new Error(running === null ? "生产单正在运行中,请先暂停或停止" : `生产单正在运行中(${describeRunningBatch(running)}),请先暂停或停止`);
|
|
9590
|
+
}
|
|
9569
9591
|
const outputDir = runDir ?? config.outputDir;
|
|
9570
9592
|
let project = loadProject(outputDir);
|
|
9571
9593
|
if (project === void 0) throw new Error("输出目录中没有项目");
|
|
@@ -9581,6 +9603,7 @@ var ProductionRunner = class {
|
|
|
9581
9603
|
}
|
|
9582
9604
|
this.state = {
|
|
9583
9605
|
runId: `run-${Date.now().toString(36)}`,
|
|
9606
|
+
bookName: project.bookName,
|
|
9584
9607
|
startNo,
|
|
9585
9608
|
endNo,
|
|
9586
9609
|
status: "running",
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 右上角「小说工坊生成状态」芯片的显示偏好(纯客户端、localStorage)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是 localStorage 而不是插件配置:这是纯界面 chrome,与显示模式 / 界面密度 /
|
|
5
|
+
* 编辑器字号 / 芯片自身的位置记忆同类;哪个浏览器想清屏就哪个浏览器清,不需要跨端同步。
|
|
6
|
+
* 将来若要跨端,把键名沿用成 config 字段 `showRunChip` 即可平滑升级。
|
|
7
|
+
*
|
|
8
|
+
* 三态:
|
|
9
|
+
* off 关闭(同时停止 4 秒轮询,不白跑)
|
|
10
|
+
* active 只在生产单**运行中或已暂停**时显示(不想错过生成,也不想留残影)
|
|
11
|
+
* always 只要该目录有生产单状态就显示(默认,保持历史行为)
|
|
12
|
+
*/
|
|
13
|
+
import type { RunState } from '../protocol.ts';
|
|
14
|
+
export type RunChipMode = 'off' | 'active' | 'always';
|
|
15
|
+
/** localStorage 键(与芯片位置记忆同一命名空间)。 */
|
|
16
|
+
export declare const RUN_CHIP_MODE_KEY = "dsh-novel-forge.runChip.mode";
|
|
17
|
+
/** 同页变更通知用的自定义事件名(跨标签页走原生 storage 事件)。 */
|
|
18
|
+
export declare const RUN_CHIP_MODE_EVENT = "dsh-novel-forge:runchip-mode";
|
|
19
|
+
/** 默认值与历史行为一致:有生产单状态就显示。 */
|
|
20
|
+
export declare const DEFAULT_RUN_CHIP_MODE: RunChipMode;
|
|
21
|
+
/** 设置页下拉用的三态选项(顺序即用户看到的顺序)。 */
|
|
22
|
+
export declare const RUN_CHIP_MODE_OPTIONS: ReadonlyArray<{
|
|
23
|
+
value: RunChipMode;
|
|
24
|
+
label: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** 读取偏好(缺失 / 非法 / 存储不可用 → 默认值)。 */
|
|
27
|
+
export declare function readRunChipMode(): RunChipMode;
|
|
28
|
+
/** 写入偏好并通知所有监听者;存储不可用时仍通知(本次会话内生效)。 */
|
|
29
|
+
export declare function writeRunChipMode(mode: RunChipMode): void;
|
|
30
|
+
/** 订阅偏好变化(同页 + 跨标签页 storage);返回退订函数。 */
|
|
31
|
+
export declare function subscribeRunChipMode(listener: (mode: RunChipMode) => void): () => void;
|
|
32
|
+
/**
|
|
33
|
+
* 给定偏好与当前生产单状态,判断芯片是否应可见。
|
|
34
|
+
* null(没有生产单状态)永远不显示——这是渲染层与测试共用的唯一判据。
|
|
35
|
+
*/
|
|
36
|
+
export declare function isRunChipVisible(mode: RunChipMode, state: RunState | null | undefined): boolean;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* 运行状态芯片:固定在页面顶部的「小说工坊生成状态」小徽标。
|
|
3
3
|
* 轮询 /run/status,实时显示 生成中/暂停/完成/停止/出错 + 当前章号;
|
|
4
4
|
* 点击可打开/关闭小说工坊面板。独立 DOM 注入,不触碰主面板逻辑,挂载失败静默降级。
|
|
5
|
+
* 显示偏好(关 / 仅生成中暂停时 / 总是)存在 localStorage,见 chip-prefs.ts:
|
|
6
|
+
* 书架页「设置 · 全局 → 外观与主题」里有对应下拉,选择即时生效(跨标签页也同步)。
|
|
5
7
|
*
|
|
6
8
|
* P0 重写说明(原实现的三个问题):
|
|
7
9
|
* 1. 整块背景染状态色 + 固定白字 → 浅色主题下「白字配亮底」对比度崩坏。
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -1497,6 +1497,12 @@ export interface StatusResponse {
|
|
|
1497
1497
|
/** 生产单状态(生产单 = 区间批量生产执行器:计划补足 + 逐章生成 + 被拒分级处理)。 */
|
|
1498
1498
|
export interface RunState {
|
|
1499
1499
|
runId: string;
|
|
1500
|
+
/**
|
|
1501
|
+
* 本批次归属的书名(可选,兼容旧 run-state.json)。
|
|
1502
|
+
* 生产单是全局单实例,而 /run/status 恢复时会扫书架取第一个有 run-state 的目录,
|
|
1503
|
+
* 因此"这是哪本书的批次"必须显式带上,否则芯片/面板只能显示一个无法归属的状态。
|
|
1504
|
+
*/
|
|
1505
|
+
bookName?: string;
|
|
1500
1506
|
/** 区间起点(含)。 */
|
|
1501
1507
|
startNo: number;
|
|
1502
1508
|
/** 区间终点(含)。 */
|
package/lib/types/run.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { NovelConfig, RunState } from './protocol.ts';
|
|
3
|
+
/**
|
|
4
|
+
* 解析某个输出目录的书名(用于旧 run-state.json 的兜底)。
|
|
5
|
+
* 优先项目文件里的 bookName,其次目录名——两者都没有时返回空串。
|
|
6
|
+
* @param outputDir - 该书输出目录。
|
|
7
|
+
*/
|
|
8
|
+
export declare function resolveRunBookName(outputDir: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* 一句话描述"正在跑的是哪本书的哪一章"。
|
|
11
|
+
* 面板状态行与 409 报错共用,避免两处各写各的(也便于单测覆盖)。
|
|
12
|
+
* @param state - 运行中的生产单状态。
|
|
13
|
+
*/
|
|
14
|
+
export declare function describeRunningBatch(state: RunState): string;
|
|
3
15
|
export declare class ProductionRunner {
|
|
4
16
|
private deps;
|
|
5
17
|
private state;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@waterwx/dsh-novel-forge",
|
|
3
3
|
"description": "AI 编译纯文本小说工作台 for the dsh web GUI: import a novel outline (docx/text), plan volumes & chapters with LLM, generate chapter by chapter (3000-4000 chars each), auto AI review & book audit, and export the full book as Markdown/TXT into your chosen output folder.",
|
|
4
|
-
"version": "1.3.
|
|
4
|
+
"version": "1.3.2-alpha",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "waterwx",
|
|
7
7
|
"repository": {
|
|
@@ -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
|
+
}
|