dsh-maoying 0.1.11 → 0.1.12
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 +15 -2
- package/bin/maoying.mjs +95 -50
- package/docs/creator-panel.md +31 -7
- package/docs/design.md +10 -7
- package/docs/spec.md +7 -4
- package/index.mjs +242 -88
- package/lib/base.css +156 -25
- package/lib/client.js +392 -120
- package/lib/epub.mjs +37 -6
- package/lib/novel-parse.mjs +171 -46
- package/lib/panel-core.mjs +116 -10
- package/lib/version.mjs +122 -36
- package/lib/yaml-loader.mjs +10 -2
- package/package.json +6 -2
- package/presets/maoying/agent.cordis.v2.yml +6 -1
- package/presets/maoying/agent.cordis.yml +5 -2
- package/presets/maoying/restrict.mjs +5 -6
- package/skills/maoying-review-checklist/SKILL.md +1 -1
- package/templates/novel/README.md +3 -3
- package/templates/novel/foreshadows.yml +1 -0
- package/templates/novel/timeline.yml +1 -1
package/README.md
CHANGED
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
- **上下文按需取**:主编先读 index.yml 概览,写到哪章读哪章(前章结尾约 1500 字 + 该章大纲 + 出场角色卡 + 活跃伏笔节选),不把整本书塞进上下文。
|
|
15
15
|
- **工具面收窄**:主编只见「角色委派 + 内部检索 + 协调」白名单,帮手各带本行工具(审校只读、考据只带联网+落笔记);每次请求的工具开销与上下文占用随之下降。
|
|
16
16
|
- **素材考据**:research 帮手联网检索历史/地理/专业领域素材,笔记落 `novel/research/`,区分史实/通说/演义并附来源——DSH 架构的红利。
|
|
17
|
-
-
|
|
17
|
+
- **创作台面板**:DSH 侧栏一键打开的全页可视化面板——概览/章节/角色/伏笔/关系图/时间线/设定/索引板/小说设置九个 tab;章节整页阅读(章纲对照 + 五维 + 状态流转)、project.yml 表单化编辑、顶栏字号五档缩放与自动新版检测。
|
|
18
|
+
- **CLI 兜底**:`maoying init / stats / index / export / brief / check` 六条零依赖命令管脚手架、统计、索引重建、导出、写作卡打包与静态体检。
|
|
18
19
|
|
|
19
20
|
## 快速开始
|
|
20
21
|
|
|
@@ -66,6 +67,16 @@ npm install -g dsh-maoying # 或装成全局 maoying 命令
|
|
|
66
67
|
|
|
67
68
|
用户回「ok / 可以 / 继续」即视为确认,主编直接推进,不复述确认。
|
|
68
69
|
|
|
70
|
+
## 创作台面板
|
|
71
|
+
|
|
72
|
+
DSH 左侧栏底部点「**毛颖创作台**」打开全页面板(改文件、跑流程之外,给肉眼看的驾驶舱):
|
|
73
|
+
|
|
74
|
+
- **章节**:状态筛选 + 按单写作清单;点章节进整页阅读——返回按钮、章纲对照(Markdown 渲染)、只读正文、五维叙事字段、一键状态流转。
|
|
75
|
+
- **小说设置**:project.yml 的书名/作者/题材/篇幅/语言/工作流阶段/共创模式/简介表单化编辑,保存保留文件里原有注释。
|
|
76
|
+
- **顶栏**:当前版本号常驻;npm 上有新版时亮「有新版 ↗」徽章直连发布页;齿轮打开系统设置,字号五档(100%–200%)整体缩放并记忆。
|
|
77
|
+
|
|
78
|
+
面板只写状态(章节状态、project.yml、index.yml 重建),不改正文——正文永远归作者与主编。
|
|
79
|
+
|
|
69
80
|
## 共创模式(作者亲笔)
|
|
70
81
|
|
|
71
82
|
不是谁都想让 AI 代笔。project.yml 的 `mode` 键决定正文归谁写,三种模式一句话:`ai-draft`(默认)AI 代笔,走 Writing 六步法;`co-write` 逐章指派谁写,主编动笔前先问清本章归谁;`author-draft` 全书作者亲笔,毛颖只备料、审稿、记账,永不代写。
|
|
@@ -110,6 +121,8 @@ maoying init [dir] # 生成 novel/ 脚手架(默认 ./novel;已存在文
|
|
|
110
121
|
maoying stats [dir] # 逐章 CJK 字数、状态分布、当前 phase
|
|
111
122
|
maoying index [dir] # 从 volumes.yml + chapters/ 重建 index.yml(派生文件)
|
|
112
123
|
maoying export [dir] # 导出全书:--format md|txt|epub(默认 md),--out <目录> 改输出位置
|
|
124
|
+
maoying brief [dir] <chapter> # 组装单章写作卡(章纲+前章结尾+角色卡+活跃伏笔);--out 落盘,缺省 stdout
|
|
125
|
+
maoying check [dir] # 静态体检(不调模型):frontmatter/枚举/章号/伏笔引用/时间线年份/关系卡引用
|
|
113
126
|
```
|
|
114
127
|
|
|
115
128
|
export 示例:
|
|
@@ -137,7 +150,7 @@ rm -rf "${DSH_HOME:-$HOME/.dsh}/skills/"maoying-*
|
|
|
137
150
|
|
|
138
151
|
## 兼容面
|
|
139
152
|
|
|
140
|
-
|
|
153
|
+
插件启动时读宿主版本,按版本自动选择委派行形状——**宿主升级后重启即切换,无需重装插件**。版本探测是五级探针(argv[1] → 全 argv 扫描 → app.asar / desktop-runtime → Electron API → home 全局兜底),前四级锚定当前进程的真实宿主,`<home>/node_modules/@deepseek-ai/dsh/package.json` 那份 home 全局安装只做最后兜底(老 CLI 残留可能陈旧几个大版本);每级读到了什么可查创作台面板的 `host-debug` 端点(路径已脱敏):
|
|
141
154
|
|
|
142
155
|
- **DSH 0.1.7 – 0.2.1-rc.1 → v1 内联形状**:5 个角色委派行直接内联 persona 与 toolFilter,托管块 1 条声明行。
|
|
143
156
|
- **DSH 0.2.1-rc.2+ → v2 AgentPreset 形状**:该版本起 dsh-tool-subagent 拒绝内联 persona/toolFilter,插件改用 [presets/maoying/agent.cordis.v2.yml](presets/maoying/agent.cordis.v2.yml)——托管块写 6 条声明行(主编 preset + 大纲师/写手/审校/设定管理员/素材调研员 5 张角色 preset),主编委派行以 `agentPreset` 指向角色 preset,角色的工具面由 preset 里的实体工具行承载(审校只读、考据带联网检索)。
|
package/bin/maoying.mjs
CHANGED
|
@@ -17,15 +17,25 @@
|
|
|
17
17
|
* 子命令出错 exit 1,中文报错。
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
|
-
import { copyFileSync, existsSync, mkdirSync, readdirSync,
|
|
20
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs'
|
|
21
21
|
import { randomUUID } from 'node:crypto'
|
|
22
22
|
import path from 'node:path'
|
|
23
23
|
import { fileURLToPath } from 'node:url'
|
|
24
24
|
|
|
25
25
|
import { buildEpub } from '../lib/epub.mjs'
|
|
26
26
|
// 解析层与面板共用(lib/novel-parse.mjs);parseFlatYamlArray 沿用旧名
|
|
27
|
-
// parseSimpleYaml,保持本文件其余代码原样不动。
|
|
28
|
-
|
|
27
|
+
// parseSimpleYaml,保持本文件其余代码原样不动。STATUSES / splitParagraphs
|
|
28
|
+
// 也改从 lib 取——状态机与分段逻辑只留 lib 一份,bin 不再各养一副拷贝。
|
|
29
|
+
import {
|
|
30
|
+
buildBrief,
|
|
31
|
+
buildIndexYaml,
|
|
32
|
+
CHAPTER_STATUSES as STATUSES, // 章节状态机(原 bin 本地 STATUSES,展示用中文标签随 lib 走)
|
|
33
|
+
checkProject,
|
|
34
|
+
collectChapters,
|
|
35
|
+
parseFlatYamlArray as parseSimpleYaml,
|
|
36
|
+
readTextOrNull, // 读文本文件读不到返回 null(原 bin 本地拷贝,lib 导出后改 import)
|
|
37
|
+
splitParagraphs, // 正文分段(原 bin 本地 bodyParagraphs 的同款实现)
|
|
38
|
+
} from '../lib/novel-parse.mjs'
|
|
29
39
|
|
|
30
40
|
const PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
31
41
|
const TEMPLATE_DIR = path.join(PLUGIN_ROOT, 'templates', 'novel')
|
|
@@ -41,14 +51,6 @@ const PHASES = {
|
|
|
41
51
|
export: '导出',
|
|
42
52
|
}
|
|
43
53
|
|
|
44
|
-
/** 章节状态机(frontmatter status 枚举;带中文标签,展示用)。 */
|
|
45
|
-
const STATUSES = [
|
|
46
|
-
['pending', '待写'],
|
|
47
|
-
['writing', '写作中'],
|
|
48
|
-
['review', '审阅中'],
|
|
49
|
-
['accepted', '已定稿'],
|
|
50
|
-
]
|
|
51
|
-
|
|
52
54
|
// ── 基础工具 ────────────────────────────────────────────────────────────────
|
|
53
55
|
|
|
54
56
|
/** 中文报错 + exit 1。 */
|
|
@@ -57,12 +59,19 @@ function fail(message) {
|
|
|
57
59
|
process.exit(1)
|
|
58
60
|
}
|
|
59
61
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
/**
|
|
63
|
+
* 同名输出不静默覆盖:目标文件已存在时在扩展名前加序号 -2、-3……直到找到
|
|
64
|
+
* 未占用的名字。返回 { path, renamed };renamed 为 true 时调用方向 stderr
|
|
65
|
+
* 提示一句改写去向——绝不覆盖用户已有文件。
|
|
66
|
+
*/
|
|
67
|
+
function dedupeWritePath(target) {
|
|
68
|
+
if (!existsSync(target)) return { path: target, renamed: false }
|
|
69
|
+
const dir = path.dirname(target)
|
|
70
|
+
const ext = path.extname(target)
|
|
71
|
+
const stem = path.basename(target, ext)
|
|
72
|
+
for (let n = 2; ; n++) {
|
|
73
|
+
const candidate = path.join(dir, `${stem}-${n}${ext}`)
|
|
74
|
+
if (!existsSync(candidate)) return { path: candidate, renamed: true }
|
|
66
75
|
}
|
|
67
76
|
}
|
|
68
77
|
|
|
@@ -94,7 +103,14 @@ function copyTemplateTree(srcDir, destDir, relBase = '') {
|
|
|
94
103
|
function cmdInit(targetArg) {
|
|
95
104
|
if (!existsSync(TEMPLATE_DIR)) fail(`模板目录缺失:${TEMPLATE_DIR}(插件包不完整?)`)
|
|
96
105
|
const dest = path.resolve(targetArg ?? 'novel')
|
|
97
|
-
|
|
106
|
+
// 落盘点兜底:目录建不出来 / 模板拷不动(权限、只读盘等)报中文错,不甩英文栈
|
|
107
|
+
let counts
|
|
108
|
+
try {
|
|
109
|
+
counts = copyTemplateTree(TEMPLATE_DIR, dest)
|
|
110
|
+
} catch (error) {
|
|
111
|
+
fail(`写入失败:${dest}(${error.message})`)
|
|
112
|
+
}
|
|
113
|
+
const { created, skipped } = counts
|
|
98
114
|
console.log(`毛颖脚手架 → ${dest}`)
|
|
99
115
|
for (const rel of created) console.log(` 创建 ${rel}`)
|
|
100
116
|
for (const rel of skipped) console.log(` 跳过 ${rel}(已存在,未覆盖)`)
|
|
@@ -152,7 +168,12 @@ function cmdIndex(targetArg) {
|
|
|
152
168
|
const volumesRaw = parseSimpleYaml(readTextOrNull(path.join(root, 'volumes.yml')))
|
|
153
169
|
const { text, volumesCount, chaptersCount, orphansCount, totalWords } = buildIndexYaml(chapters, volumesRaw)
|
|
154
170
|
const indexPath = path.join(root, 'index.yml')
|
|
155
|
-
|
|
171
|
+
// 落盘点兜底:写不进(权限/只读盘等)报中文错,不甩英文栈
|
|
172
|
+
try {
|
|
173
|
+
writeFileSync(indexPath, text, 'utf8')
|
|
174
|
+
} catch (error) {
|
|
175
|
+
fail(`写入失败:${indexPath}(${error.message})`)
|
|
176
|
+
}
|
|
156
177
|
console.log(`已重建 ${indexPath}:${volumesCount} 卷、${chaptersCount} 章(总字数 ${totalWords})、未归卷 ${orphansCount} 章。`)
|
|
157
178
|
}
|
|
158
179
|
|
|
@@ -190,26 +211,6 @@ function volumeNumber(volume) {
|
|
|
190
211
|
return m === null ? null : Number(m[1])
|
|
191
212
|
}
|
|
192
213
|
|
|
193
|
-
/**
|
|
194
|
-
* 正文分段:空行为段界;段内多行去首尾空白后直接拼接(中文行间不加空格)。
|
|
195
|
-
* md/txt 的正文与 epub 的 <p> 共用这一套。
|
|
196
|
-
*/
|
|
197
|
-
function bodyParagraphs(body) {
|
|
198
|
-
const paragraphs = []
|
|
199
|
-
let current = []
|
|
200
|
-
for (const rawLine of String(body ?? '').split(/\r?\n/)) {
|
|
201
|
-
const line = rawLine.trim()
|
|
202
|
-
if (line === '') {
|
|
203
|
-
if (current.length > 0) paragraphs.push(current.join(''))
|
|
204
|
-
current = []
|
|
205
|
-
} else {
|
|
206
|
-
current.push(line)
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (current.length > 0) paragraphs.push(current.join(''))
|
|
210
|
-
return paragraphs
|
|
211
|
-
}
|
|
212
|
-
|
|
213
214
|
/**
|
|
214
215
|
* 章节展示信息(md/txt/epub 三格式共用):卷层、章标题、分段正文。
|
|
215
216
|
* 章标题形如「卷X-章Y 章节标题」;卷 id 不合规范或章节无卷信息时退为
|
|
@@ -223,7 +224,7 @@ function chapterDisplay(ch, ordinal, volTitles) {
|
|
|
223
224
|
volume: volNum !== null ? { num: volNum, title: volTitles.get(ch.volume) ?? '' } : null,
|
|
224
225
|
heading: `${volPart} ${ch.title}`.trim(),
|
|
225
226
|
body: ch.body,
|
|
226
|
-
paragraphs:
|
|
227
|
+
paragraphs: splitParagraphs(ch.body),
|
|
227
228
|
}
|
|
228
229
|
}
|
|
229
230
|
|
|
@@ -296,6 +297,8 @@ function cmdExport(args) {
|
|
|
296
297
|
}
|
|
297
298
|
}
|
|
298
299
|
if (!EXPORT_FORMATS.includes(format)) fail(`未知导出格式「${format}」——支持 ${EXPORT_FORMATS.join(' / ')}`)
|
|
300
|
+
// 显式拒绝空串:`--out=` 经 path.resolve 会静默落成 cwd,属意外行为
|
|
301
|
+
if (outArg !== null && outArg.trim() === '') fail('选项 --out 不能是空串——--out 取导出目录路径')
|
|
299
302
|
|
|
300
303
|
const root = path.resolve(dirArg ?? 'novel')
|
|
301
304
|
const chapters = collectChapters(root)
|
|
@@ -316,6 +319,9 @@ function cmdExport(args) {
|
|
|
316
319
|
const stamp = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}`
|
|
317
320
|
const outDir = path.resolve(outArg ?? path.join(root, 'exports'))
|
|
318
321
|
const outPath = path.join(outDir, `${sanitizeTitleForFile(rawTitle)}-${stamp}.${format}`)
|
|
322
|
+
// 同名输出不静默覆盖:目标已存在时自动加序号(书名-20250101-2.epub)并提示
|
|
323
|
+
const target = dedupeWritePath(outPath)
|
|
324
|
+
if (target.renamed) console.error(`提示:${outPath} 已存在,本次导出改写到 ${target.path}`)
|
|
319
325
|
|
|
320
326
|
let content
|
|
321
327
|
if (format === 'md') {
|
|
@@ -331,10 +337,15 @@ function cmdExport(args) {
|
|
|
331
337
|
})
|
|
332
338
|
}
|
|
333
339
|
|
|
334
|
-
|
|
335
|
-
|
|
340
|
+
// 落盘点兜底:目录建不出 / 文件写不进(权限、只读盘等)报中文错,不甩英文栈
|
|
341
|
+
try {
|
|
342
|
+
mkdirSync(outDir, { recursive: true })
|
|
343
|
+
writeFileSync(target.path, content)
|
|
344
|
+
} catch (error) {
|
|
345
|
+
fail(`写入失败:${target.path}(${error.message})`)
|
|
346
|
+
}
|
|
336
347
|
const totalWords = chapters.reduce((sum, ch) => sum + ch.words, 0)
|
|
337
|
-
console.log(`导出完成:${path.resolve(
|
|
348
|
+
console.log(`导出完成:${path.resolve(target.path)}`)
|
|
338
349
|
console.log(`格式 ${format};${chapters.length} 章,总字数 ${totalWords}(CJK 计数)。`)
|
|
339
350
|
}
|
|
340
351
|
|
|
@@ -373,6 +384,8 @@ function cmdBrief(args) {
|
|
|
373
384
|
if (dirArg === null && chapterArg === null) {
|
|
374
385
|
fail('用法:maoying brief [dir] <chapter> [--out <文件>]——chapter 可写文件名(v01-c002.md)、stem(v01-c002)、「卷-章」(1-2)或裸章号(2)')
|
|
375
386
|
}
|
|
387
|
+
// 显式拒绝空串:`--out=` 经 path.resolve 会静默落成 cwd,属意外行为
|
|
388
|
+
if (outArg !== null && outArg.trim() === '') fail('选项 --out 不能是空串——--out 取输出文件路径')
|
|
376
389
|
// 只给一个位置参数时把它当章号用(dir 走默认 ./novel),这是常用路径
|
|
377
390
|
const root = path.resolve(chapterArg === null ? 'novel' : dirArg)
|
|
378
391
|
const brief = buildBrief(root, chapterArg ?? dirArg)
|
|
@@ -380,9 +393,17 @@ function cmdBrief(args) {
|
|
|
380
393
|
for (const warning of brief.warnings) console.error(`提醒:${warning}`)
|
|
381
394
|
if (outArg !== null) {
|
|
382
395
|
const outPath = path.resolve(outArg)
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
console.
|
|
396
|
+
// 同名输出不静默覆盖:目标已存在时自动加序号 -2、-3……改写到新路径
|
|
397
|
+
const target = dedupeWritePath(outPath)
|
|
398
|
+
if (target.renamed) console.error(`提示:${outPath} 已存在,本次写作卡改写到 ${target.path}`)
|
|
399
|
+
// 落盘点兜底:目录建不出 / 文件写不进(权限、只读盘等)报中文错,不甩英文栈
|
|
400
|
+
try {
|
|
401
|
+
mkdirSync(path.dirname(target.path), { recursive: true })
|
|
402
|
+
writeFileSync(target.path, brief.text, 'utf8')
|
|
403
|
+
} catch (error) {
|
|
404
|
+
fail(`写入失败:${target.path}(${error.message})`)
|
|
405
|
+
}
|
|
406
|
+
console.log(`写作卡已写入:${target.path}`)
|
|
386
407
|
console.log('拿去写,写完丢回给主编收稿。')
|
|
387
408
|
} else {
|
|
388
409
|
process.stdout.write(brief.text)
|
|
@@ -434,18 +455,42 @@ const HELP_TEXT = `毛颖(maoying)—— 网文创作项目 CLI
|
|
|
434
455
|
check 有 error 退出码 1,只有 warn 退出码 0。
|
|
435
456
|
所有命令零依赖,任意 cwd 可用;出错时退出码 1。`
|
|
436
457
|
|
|
458
|
+
/** 只接受一个可选目录参数的子命令(目录参数不以 `-` 开头;--help/-h 由 main 先拦)。 */
|
|
459
|
+
const SINGLE_DIR_COMMANDS = ['init', 'stats', 'index', 'check']
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* 单参命令的目录参数防线:至多一个位置参数,且不以 `-` 开头——以前
|
|
463
|
+
* `maoying init --help` 把 --help 当目录名,静默建出 ./--help/。--help/-h 由
|
|
464
|
+
* main 在派发前拦截显示帮助;其余 `-` 开头参数报未知选项,多余位置参数报错
|
|
465
|
+
* (对照 export 的同款防线)。
|
|
466
|
+
*/
|
|
467
|
+
function singleDirTarget(command, rest) {
|
|
468
|
+
let target = null
|
|
469
|
+
for (const arg of rest) {
|
|
470
|
+
if (arg.startsWith('-')) fail(`未知选项「${arg}」——${command} 只接受一个可选目录参数`)
|
|
471
|
+
if (target !== null) fail(`多余的位置参数「${arg}」——${command} 只接受一个目录参数`)
|
|
472
|
+
target = arg
|
|
473
|
+
}
|
|
474
|
+
return target
|
|
475
|
+
}
|
|
476
|
+
|
|
437
477
|
function main(argv) {
|
|
438
478
|
const [command, ...rest] = argv
|
|
439
479
|
if (command === '--help' || command === '-h' || command === 'help') {
|
|
440
480
|
console.log(HELP_TEXT)
|
|
441
481
|
return
|
|
442
482
|
}
|
|
443
|
-
|
|
444
|
-
if (command === '
|
|
445
|
-
|
|
483
|
+
// 单参子命令自身也认 --help/-h(防 init --help 被当目录名),显示帮助即止
|
|
484
|
+
if (SINGLE_DIR_COMMANDS.includes(command) && rest.some((a) => a === '--help' || a === '-h')) {
|
|
485
|
+
console.log(HELP_TEXT)
|
|
486
|
+
return
|
|
487
|
+
}
|
|
488
|
+
if (command === 'init') return cmdInit(singleDirTarget('init', rest))
|
|
489
|
+
if (command === 'stats') return cmdStats(singleDirTarget('stats', rest))
|
|
490
|
+
if (command === 'index') return cmdIndex(singleDirTarget('index', rest))
|
|
446
491
|
if (command === 'export') return cmdExport(rest)
|
|
447
492
|
if (command === 'brief') return cmdBrief(rest)
|
|
448
|
-
if (command === 'check') return cmdCheck(rest
|
|
493
|
+
if (command === 'check') return cmdCheck(singleDirTarget('check', rest))
|
|
449
494
|
if (command === undefined) {
|
|
450
495
|
console.error('maoying: 缺少子命令(init / stats / index / export / brief / check,--help 看用法)')
|
|
451
496
|
process.exit(1)
|
package/docs/creator-panel.md
CHANGED
|
@@ -37,16 +37,31 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
37
37
|
|
|
38
38
|
| 路由 | 方法 | 入参 | 出参 |
|
|
39
39
|
|---|---|---|---|
|
|
40
|
-
| `client.css` | GET | — | text/css(lib/base.css
|
|
40
|
+
| `client.css` | GET | — | text/css(lib/base.css 现读,no-cache) |
|
|
41
|
+
| `update-check` | GET | — | `{ ok, current, latest, updateAvailable, releaseUrl, checkedAt, error, source }`——新版检测(npm registry → npmmirror → GitHub 三级源,1h 缓存 + 7 天 stale-if-error,全败静默降级为 latest=null / error 非空) |
|
|
42
|
+
| `host-debug` | GET | — | `{ ok, hostVersion, trace, shape, registry, runtime }`——现场诊断(见下节) |
|
|
41
43
|
| `workspaces` | GET | — | `{ ok, workspaces: [{ path, name, hasNovel }] }`——workspaceRegistry.list() 逐根探测 `<root>/novel/project.yml` |
|
|
42
44
|
| `state` | GET | `path=<root>` | 全量快照(见 §3 契约) |
|
|
43
45
|
| `chapter` | GET | `path`, `file`(chapters/ 下文件名) | `{ ok, file, frontmatter, body, outline }`——正文全文 + 对应章纲(`{ file, words, body }`,outlines/ 无对应文件时 null),章节抽屉用 |
|
|
44
46
|
| `chapter-status` | POST | `{ path, file, status }` | `{ ok }`——frontmatter status 行原地替换;status 必须 ∈ pending/writing/review/accepted |
|
|
45
47
|
| `foreshadow-status` | POST | `{ path, id, status }` | `{ ok }`——foreshadows.yml 对应条目 status 行替换;∈ planted/progressing/resolved/abandoned |
|
|
46
48
|
| `phase` | POST | `{ path, phase }` | `{ ok }`——project.yml phase 行替换;七态枚举 |
|
|
49
|
+
| `project-save` | POST | `{ path, values }`(8 平键,title/author/genre/length/language/phase/mode/description) | `{ ok }`——mutateProject 逐键改写保行尾注释、缺键按模板补齐;phase/mode 越枚举与空书名 422 |
|
|
47
50
|
| `rebuild-index` | POST | `{ path }` | `{ ok, chapters }`——照 CLI index 同一逻辑重建 index.yml |
|
|
48
51
|
|
|
49
|
-
|
|
52
|
+
共 11 条。写操作全部「行级原地替换」:只在 frontmatter/目标 yml 的对应行上做字符串手术,写前读、写后校验(替换计数 ≠1 即 409 拒写并报错),不做整文件重排——把对 git diff 的扰动压到最小,也与 agent 的写法兼容。
|
|
53
|
+
|
|
54
|
+
### host-debug 端点(现场诊断)
|
|
55
|
+
|
|
56
|
+
用途:面板「有新版不显示 / 形态判定可疑」时现场定位宿主版本读到的是哪一份。响应:
|
|
57
|
+
|
|
58
|
+
- `hostVersion`:五级探针(argv[1] → 全 argv 扫描 → app.asar / desktop-runtime → Electron API → home 全局兜底)命中的版本;全落空为 null。
|
|
59
|
+
- `trace`:每级探针一条轨迹 `{ probe, hit, via?, note? }`,能看到每级看到了什么、为什么落空。
|
|
60
|
+
- `shape`:托管块形态判定(v1/v2,读不到宿主版本保守 v1)。
|
|
61
|
+
- `registry`:预设注册表内窥(逐 preset 的 broken 激活诊断,尽力而为)。
|
|
62
|
+
- `runtime`:argv / execPath / cwd / electron / node。
|
|
63
|
+
|
|
64
|
+
围栏与脱敏:与所有路由同一信任围栏(回环 + 可信 Host);出口统一 sanitizeDeep——trace.via、runtime.argv / execPath / cwd 等本机绝对路径一律换 `~`,诊断信息照给、路径不出门。
|
|
50
65
|
|
|
51
66
|
## 3. state 快照契约(前后端共同遵守,字段名冻结)
|
|
52
67
|
|
|
@@ -56,7 +71,7 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
56
71
|
"root": "<workspace root>",
|
|
57
72
|
"novelDir": "<root>/novel",
|
|
58
73
|
"found": true, // false 时其余字段缺省,面板显示「未初始化」引导
|
|
59
|
-
"project": { "title","author","genre","length","language","phase","description" },
|
|
74
|
+
"project": { "title","author","genre","length","language","phase","mode","description" }, // mode: ai-draft|co-write|author-draft(五期)
|
|
60
75
|
"chapters": [{ "file","volume","num","title","status","words","summary",
|
|
61
76
|
"cognitive_frame","emotional_anchor","world_texture",
|
|
62
77
|
"suspense","interpersonal_tension" }], // 按 volume,num 升序;words 为 CJK 计数
|
|
@@ -88,12 +103,12 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
88
103
|
- 会话中栏全页 overlay:`html[data-dsh-maoying-active]` 驱动,`dsh-panel-activate` 事件与 mnemon/taskboard/ssh 等全页面板互斥,点侧栏会话行自动关闭;
|
|
89
104
|
- 顶部工作区选择器(workspaces 路由取数,hasNovel 的排前;无 novel 工作区显示引导卡:一句 `maoying init` 提示 + 复制按钮)。
|
|
90
105
|
|
|
91
|
-
|
|
106
|
+
九个 tab(顺序即默认排布,第 9 个为后续迭代新增):
|
|
92
107
|
|
|
93
108
|
1. **概览**:project 卡(书名大字 + 类型/篇幅/语言 pills + 阶段徽章 + 简介);统计卡(总字数/章节数/状态分布条);最近更新(chapters 按 mtime 前 5);warnings 黄条。
|
|
94
|
-
2. **章节**:表格(卷-章/标题/状态徽章/字数/梗概),状态筛选 pills;表格上方「按单写作」区块——有章纲且 status=pending 的章按序列出(章纲节选 +
|
|
109
|
+
2. **章节**:表格(卷-章/标题/状态徽章/字数/梗概),状态筛选 pills;表格上方「按单写作」区块——有章纲且 status=pending 的章按序列出(章纲节选 + 状态徽章,共创模式的主工作清单);行点击开整页阅读视图(0.1.12 起替代原右侧抽屉——fixed 侧弹层会被 DSH 侧边栏遮挡):返回按钮 + GET chapter 正文(只读,等宽滚动)+ 章纲对照栏(左右分栏或折叠段)+ 复制文件路径按钮 + 五维字段卡 + 状态流转按钮(当前态 → 下一态一键,pending→writing→review→accepted)。
|
|
95
110
|
3. **角色**:卡墙(姓名/role 徽章四色/年龄性别/性格一句/动机一行),点击看全卡(含 arc/markers/background)。
|
|
96
|
-
4. **伏笔**:四列看板(埋设/推进/已回收/弃用),卡片含 priority 色点(high 红/normal 灰/low 淡)、`source: backfill` 的「回填」小徽章(账本回填来的条目,区别于写前规划)、埋设章→预期回收章、notes
|
|
111
|
+
4. **伏笔**:四列看板(埋设/推进/已回收/弃用),卡片含 priority 色点(high 红/normal 灰/low 淡)、`source: backfill` 的「回填」小徽章(账本回填来的条目,区别于写前规划)、埋设章→预期回收章、notes 全文直显(0.1.10 去掉两行截断);卡片按钮:推进到下一状态(planted→progressing→resolved;abandoned 只能从看板拖不动、按钮给「弃用」)。列头计数。
|
|
97
112
|
5. **关系图**:canvas 力导向图——节点=角色(radius 随出场无关、固定 18px,名字 label 下方),边=relations(intensity 1-5 → 线宽 1-4px,type 标在边中点 hover 显示 description);拖拽节点、空白处滚轮缩放、双击复位;intensity 缺省 3。角色多于 30 个时只画与 relations 相连的节点,其余进「未入图」列表。
|
|
98
113
|
6. **时间线**:垂直轴,importance 1-5 → 点大小/颜色深浅,year 左列右卡片。
|
|
99
114
|
7. **设定**:world 按 category 分组列表 + science 三色标签(known 绿/extrapolation 黄/hypothesis 紫,标签下 references 链接列表)。
|
|
@@ -101,8 +116,17 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
101
116
|
|
|
102
117
|
状态徽章色(全局统一):pending 灰 / writing 蓝 / review 橙 / accepted 绿;phase 七态沿用七色 pill。文案全中文、禁 emoji 图标(用 ui-primitives 图标或纯 CSS)。
|
|
103
118
|
|
|
119
|
+
## 4.1 后续迭代记录(0.1.10 – 0.1.12)
|
|
120
|
+
|
|
121
|
+
- **MaskPortal**:抽屉/弹层统一 portal 到 `document.body`——面板容器挂在宿主会话中栏,中栏 transform/overflow 会把 `position: fixed` 劫持成相对自身并连同裁剪(桌面端「点了没反应」实测病灶);无 createPortal 的老宿主退回原地渲染。
|
|
122
|
+
- **顶栏版本区**:当前版本号常驻;`update-check` 路由(模仿 dsh-paoding:npm registry → npmmirror → GitHub 三级源,1h 缓存 + 7 天 stale-if-error,全败静默)发现新版时亮「有新版 ↗」徽章。
|
|
123
|
+
- **系统设置**:齿轮弹窗,字号五档(100/120/150/180/200%)——面板根与 portal 节点读同一 `--myt-zoom` 变量整体 zoom,localStorage 记忆。
|
|
124
|
+
- **Markdown 渲染**:零依赖渲染器(先整体转义再变换),章纲对照与世界观/科学设定描述不再裸显 md 源码;正文手稿仍走 pre。
|
|
125
|
+
- **章节整页阅读**:替代右侧抽屉(侧边栏遮挡问题),返回按钮 + 章纲对照 + 五维 + 状态流转原位保留;阅读态隐藏 tab 条。
|
|
126
|
+
- **小说设置 tab**(第 9 个):project.yml 平键表单化编辑,`POST project-save` → `mutateProject` 逐键改写保行尾注释、缺键按模板补齐、phase/mode 越枚举与空书名 422。
|
|
127
|
+
|
|
104
128
|
## 5. 验收基线
|
|
105
129
|
|
|
106
130
|
- Node 侧:临时 workspace(/tmp)+ 假 workspaceRegistry 不行——路由从 ctx.workspaceRegistry 取;单测直接 import lib/panel-core.mjs 的函数(parseStateSnapshot / mutateChapterStatus / mutateForeshadowStatus / mutatePhase / rebuildIndex)跑断言;路由层用 node 起一个最小 ctx 假件(webServer.register 收集 handler,workspaceRegistry 返回临时 root)模拟请求验证围栏 403 / 状态流转 / 防穿越。
|
|
107
|
-
- 浏览器侧:`node --check` 不适用(browser bundle 用 var/require factory
|
|
131
|
+
- 浏览器侧:`node --check` 不适用(browser bundle 用 var/require factory 语法)——冒烟脚本 `npm run smoke`(scripts/smoke-client.mjs,零依赖)已落地:new Function 包裹整份源码做编译检查 → stub window/document 求值截获 `__ModuleLoader__.load` → factory 传 react/UI 原语 stub 求值,断言导出面 `{ inject: ["slots"], apply }` 与 slots 注入 / disposer 折返语义。CI(Node 18/20/22 矩阵)与 `npm test` 同跑;真浏览器验收仍由用户重启后做。
|
|
108
132
|
- 不写死任何真实路径;测试全在 /tmp;真实 ~/.dsh 不碰。
|
package/docs/design.md
CHANGED
|
@@ -27,7 +27,7 @@ dsh-maoying/
|
|
|
27
27
|
agent.cordis.yml # 完整组合:主编人设 + 5 角色委派 + 工具行
|
|
28
28
|
restrict.mjs # 主编工具面白名单(system-prompt/assemble 瀑布)
|
|
29
29
|
delegation-labels.mjs # 委派标签带角色显示名(tools/pre-execute 瀑布)
|
|
30
|
-
bin/maoying.mjs # CLI:init / stats / index
|
|
30
|
+
bin/maoying.mjs # CLI:init / stats / index / export / brief / check
|
|
31
31
|
templates/novel/ # init 脚手架模板
|
|
32
32
|
docs/ # design.md(本文)、spec.md(数据规范)
|
|
33
33
|
```
|
|
@@ -52,7 +52,7 @@ dsh-maoying/
|
|
|
52
52
|
plugins: <agent.cordis.yml 解析后的完整数组>
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
- 全程幂等:标记块已存在且版本一致则跳过;不一致则整块替换。YAML
|
|
55
|
+
- 全程幂等:标记块已存在且版本一致则跳过;不一致则整块替换。YAML 解析/序列化运行时从 dsh home 惰性借宿主自带的 `yaml` 包(lib/yaml-loader.mjs 多级探针),借不到回退内置极简解析(JSON 单行回退同样合法 YAML)——仓库零 npm 依赖,dependencies 恒为空。
|
|
56
56
|
- 按宿主版本自动选委派行形状:≥0.2.1-rc.2 写 v2 AgentPreset 六行块(agent.cordis.v2.yml),否则写 v1 内联单行块;宿主升降级重启即整块切换,不再需要任何降级提示(形态依据见 §8,支持面见 §6)。
|
|
57
57
|
3. 卸载 `dsh plugin remove dsh-maoying` 后:bundle 行消失,托管块与 .agent-presets/maoying/ 需手动删(README 写明两条 rm 命令)。
|
|
58
58
|
|
|
@@ -73,7 +73,7 @@ dsh-maoying/
|
|
|
73
73
|
| 上下文注入(全量角色+章节+伏笔) | 改为按需读:主编先读 index.yml 概览,写到哪章读哪章的上下文(前章结尾+该章大纲+出场角色卡+活跃伏笔) | 主编人设纪律 |
|
|
74
74
|
| AI 工具集 function calling | 天然消失(agent 直接用文件工具) | DSH 架构红利 |
|
|
75
75
|
| 一致性检查(冲突/时间线/伏笔/设定) | review 角色(审校):四维扫描 + 审稿清单 | 委派角色 |
|
|
76
|
-
| 导出 EPUB/
|
|
76
|
+
| 导出 EPUB/MD/TXT | **已交付**:`maoying export`(见 §8);HTML 从未规划 | CLI |
|
|
77
77
|
| Token 用量统计 | 无需(DSH 自带) | — |
|
|
78
78
|
| Web 管理面板 | **三期** | — |
|
|
79
79
|
| 素材考据调研 | **research 角色**:web 检索历史/地理/专业素材,笔记落 novel/research/ | 委派角色(DSH 差异化优势) |
|
|
@@ -83,7 +83,7 @@ dsh-maoying/
|
|
|
83
83
|
```
|
|
84
84
|
novel/
|
|
85
85
|
README.md # 数据规范速查(init 时从模板生成;主编不确定格式时读它)
|
|
86
|
-
project.yml # title/author/genre/length/language/phase/description
|
|
86
|
+
project.yml # title/author/genre/length/language/phase/mode/description
|
|
87
87
|
index.yml # 派生概览(maoying index 重建;主编每次开工先读)
|
|
88
88
|
volumes.yml # - {id: v01, title, summary, chapters: "001-020"}
|
|
89
89
|
chapters/
|
|
@@ -105,7 +105,7 @@ novel/
|
|
|
105
105
|
clue-board.yml # - {kind: clue|red-herring|deduction|question, content,
|
|
106
106
|
# related_chapter, resolved: bool}
|
|
107
107
|
research/ # research 角色考据笔记落这里(<topic>.md)
|
|
108
|
-
exports/ #
|
|
108
|
+
exports/ # maoying export 导出产物(默认 <dir>/exports/,--out 可改)
|
|
109
109
|
```
|
|
110
110
|
|
|
111
111
|
要点:五维字段键名用英文(cognitive_frame / emotional_anchor / world_texture / suspense / interpersonal_tension,YAML 工具安全),值中文;模板文件内注释给中文说明。frontmatter 用 `---` 围栏。所有 yml 顶层是数组或平键,禁止深嵌套——grep 友好。
|
|
@@ -162,14 +162,17 @@ persona prefix 内容骨架(agent.cordis.yml 内联,中文书写):
|
|
|
162
162
|
- `maoying init [dir]`:从 templates/novel/ 脚手架生成 novel/(已存在文件跳过,绝不覆盖),README.md + 各 yml 带中文注释示例。
|
|
163
163
|
- `maoying stats [dir]`:逐章与总字数(CJK 计数)、状态分布、phase 展示。
|
|
164
164
|
- `maoying index [dir]`:从 chapters/ 重建 index.yml(卷/章/标题/状态/字数/一句话梗概)。
|
|
165
|
-
-
|
|
165
|
+
- `maoying export [dir]`:导出全书为 md / txt / epub(默认 md;`--out` 改输出目录,缺省 `<dir>/exports/`;同名自动加序号不覆盖)。
|
|
166
|
+
- `maoying brief [dir] <chapter>`:组装单章写作卡(章纲 + 前章结尾 + 出场角色卡 + 活跃伏笔);`--out <文件>` 落盘,缺省打到 stdout。
|
|
167
|
+
- `maoying check [dir]`:静态体检(不调模型),与面板/规范同一套规则(frontmatter 完整性、枚举、章号、伏笔引用、时间线年份、关系卡引用)。
|
|
168
|
+
- 主编人设里写明:定稿维护后由 lore 角色直接重写 index.yml(文件规范写明格式),CLI `maoying index` 仅供人工兜底;stats / init 供人工使用。
|
|
166
169
|
|
|
167
170
|
## 8. 二期路线
|
|
168
171
|
|
|
169
172
|
- ✅ **技能包(黄金三章/爽点节奏/审校清单/伏笔管理)**——已做:skills/ 四目录随包分发,安装器复制到 `<home>/skills/maoying-*`(逐字节比较幂等 + .installed-version 标记,见 index.mjs installSkills)。
|
|
170
173
|
- ✅ **导出(MD/TXT/EPUB)**——已做:`maoying export [dir] --format md|txt|epub [--out <目录>]`;EPUB 由 lib/epub.mjs 零依赖打包(手写 stored zip + CRC32 表格法,EPUB 3 最小合法结构)。
|
|
171
174
|
- ✅ **0.2.1-rc.2+ AgentPreset 形态适配**——已做:presets/maoying/agent.cordis.v2.yml(主编 + 5 角色 preset 的 v2 组合源)+ index.mjs composeManagedRows 按宿主版本双形态切换(v1 内联 / v2 AgentPreset,宿主升降级重启即整块换形状;防漂移由 test/composition.test.mjs 逐字节断言,见 README「兼容面」)。
|
|
172
|
-
- ✅ **Web 创作台面板**——已做:/api/maoying/*
|
|
175
|
+
- ✅ **Web 创作台面板**——已做:/api/maoying/* 十一条路由 + 左侧栏入口 + 会话中栏全页面板,九视图(概览/章节/角色/伏笔看板/关系图/时间线/设定/索引板/小说设置);设计与契约见 docs/creator-panel.md。
|
|
173
176
|
|
|
174
177
|
## 9. 五期:共创模式(作者亲笔稿)
|
|
175
178
|
|
package/docs/spec.md
CHANGED
|
@@ -93,7 +93,7 @@ planted ──→ progressing ──→ resolved
|
|
|
93
93
|
`kind` 四类:`clue`(线索)`red-herring`(误导)`deduction`(推理)`question`(悬问)。`resolved: true` 表示该线索的戏剧功能已兑现——误导被揭示、悬问被解答、推理被验证。红鲱鱼(red-herring)必须有意埋设,`content` 里写明「误导什么、真答案是什么」,防止审校误报。
|
|
94
94
|
|
|
95
95
|
### research/ 与 exports/
|
|
96
|
-
`research/<topic>.md` 由素材调研员(research 角色)独占写入;frontmatter 记 `topic` 与 `sources`(URL 列表),body 是可直接用于写作的结论要点,区分史实/通说/演义并逐条附来源。`exports/`
|
|
96
|
+
`research/<topic>.md` 由素材调研员(research 角色)独占写入;frontmatter 记 `topic` 与 `sources`(URL 列表),body 是可直接用于写作的结论要点,区分史实/通说/演义并逐条附来源。`exports/` 是 `maoying export` 的默认产物目录(`--out` 可改到别处):`maoying export [dir] --format md|txt|epub`,文件名 `<书名>-<YYYYMMDD>.<ext>`,同名自动加序号不覆盖。
|
|
97
97
|
|
|
98
98
|
### 作者稿圣域(共创模式)
|
|
99
99
|
`chapters/` 目录的正文唯作者可改。author-draft / co-write 的 author 章节上,agent(主编与帮手)只可更新 frontmatter 的 `summary` 与五维字段,且以建议为先、动笔前说明;review 一律只出意见清单不改稿。`ai-draft` 章节不受此限,write 照旧。
|
|
@@ -113,7 +113,7 @@ volumes:
|
|
|
113
113
|
title: 第一卷 北境风雪
|
|
114
114
|
summary: 卷定位一句话
|
|
115
115
|
chapters:
|
|
116
|
-
|
|
116
|
+
- {num: 1, title: 雪夜来客, status: accepted, words: 3210, summary: 一句话梗概}
|
|
117
117
|
orphans:
|
|
118
118
|
- {volume: v02, num: 5, title: 未归卷章, status: writing, words: 1200, summary: ...}
|
|
119
119
|
total: {chapters: 21, words: 65210, accepted: 18}
|
|
@@ -128,6 +128,9 @@ total: {chapters: 21, words: 65210, accepted: 18}
|
|
|
128
128
|
|
|
129
129
|
- `maoying init` 生成上述全部骨架(已存在文件绝不覆盖);
|
|
130
130
|
- `maoying stats` 读 project.yml + chapters/,报告逐章 CJK 字数、状态分布、当前 phase;
|
|
131
|
-
- `maoying index` 是第 3 节重建逻辑的人工兜底(主编的日常路径是让 lore
|
|
131
|
+
- `maoying index` 是第 3 节重建逻辑的人工兜底(主编的日常路径是让 lore 角色直接重建);
|
|
132
|
+
- `maoying export [dir]` 导出全书为 md / txt / epub(默认 md;`--out` 改输出目录,缺省 `<dir>/exports/`);
|
|
133
|
+
- `maoying brief [dir] <chapter>` 组装单章写作卡(章纲 + 前章结尾 + 出场角色卡 + 活跃伏笔);`--out <文件>` 落盘,缺省打到 stdout;
|
|
134
|
+
- `maoying check [dir]` 静态体检(不调模型):frontmatter 完整性、枚举值、章号卷号一致、伏笔章号引用、时间线年份、关系卡引用。
|
|
132
135
|
|
|
133
|
-
|
|
136
|
+
六者都零依赖,解析器只认本规范的两类形态(平键映射、顶层数组);深度嵌套不在支持范围——这是规范约束的执行手段之一。
|