dsh-maoying 0.1.18 → 0.1.20

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/index.mjs CHANGED
@@ -64,16 +64,24 @@ import { fileURLToPath } from 'node:url'
64
64
 
65
65
  import {
66
66
  createChapterFromOutline,
67
+ createManagedFile,
68
+ deleteManagedFile,
67
69
  listWorkspaces,
70
+ mutateChapterBody,
68
71
  mutateChapterStatus,
69
72
  mutateForeshadowStatus,
73
+ mutateOutlineBody,
70
74
  mutatePhase,
71
75
  mutateProject,
72
76
  parseStateSnapshot,
73
77
  readChapter,
78
+ readManagedFile,
74
79
  readOutlineFile,
80
+ readStyleState,
75
81
  rebuildIndex,
76
82
  resolveNovelRoot,
83
+ saveManagedFile,
84
+ saveStyleText,
77
85
  } from './lib/panel-core.mjs'
78
86
  // 静态体检(check 路由)与 CLI check 同源:直接复用 novel-parse 的检查器。
79
87
  import { checkProject } from './lib/novel-parse.mjs'
@@ -942,7 +950,7 @@ export async function registerPrefixRouteWithRetry(webServer, route, { attempts
942
950
  }
943
951
 
944
952
  /**
945
- * 注册创作台全部路由(docs/creator-panel.md §2 的 15 条)。返回宿主 disposer;
953
+ * 注册创作台全部路由(docs/creator-panel.md §2 的 23 条)。返回宿主 disposer;
946
954
  * webServer 缺席(非 web 部署)时抛错,由 apply 侧统一降级为 warn。
947
955
  */
948
956
  export async function registerCreatorRoutes(ctx) {
@@ -1102,7 +1110,8 @@ export async function registerCreatorRoutes(ctx) {
1102
1110
  if (route === 'chapter' && req.method === 'GET') {
1103
1111
  // 章节正文全文(抽屉用):file 只认 chapters/ 下纯文件名;响应组装
1104
1112
  // 直传 readChapter 结果(五期起含对应章纲 outline 字段,无章纲为
1105
- // null),照 §2 契约 { ok, file, frontmatter, body, outline }。
1113
+ // null;0.1.19 起顶层与 outline 各带原文 hash——正文/章纲编辑保存
1114
+ // 的 base_hash 基线),照 §2 契约 { ok, file, frontmatter, body, hash, outline }。
1106
1115
  const novelDir = resolveNovelRoot(url.searchParams.get('path'), ctx.workspaceRegistry)
1107
1116
  json(res, 200, readChapter(novelDir, url.searchParams.get('file')))
1108
1117
  return
@@ -1117,6 +1126,16 @@ export async function registerCreatorRoutes(ctx) {
1117
1126
  return
1118
1127
  }
1119
1128
 
1129
+ if (route === 'style' && req.method === 'GET') {
1130
+ // 写作风格基准(0.1.19,写作风格 tab 取数):novel/style.md 全文 +
1131
+ // 指纹(保存路由 base_hash 的基线)+ 插件模板全文(客户端用它预填
1132
+ // 默认基线创建)。found:false = 文件尚未创建,是常态不是错误。
1133
+ const novelDir = resolveNovelRoot(url.searchParams.get('path'), ctx.workspaceRegistry)
1134
+ const style = readStyleState(novelDir)
1135
+ json(res, 200, { ok: true, found: style.found, text: style.text, hash: style.hash, template: style.template })
1136
+ return
1137
+ }
1138
+
1120
1139
  if (route === 'chapter-status' && req.method === 'POST') {
1121
1140
  const body = await readBody(req, res)
1122
1141
  const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
@@ -1157,6 +1176,75 @@ export async function registerCreatorRoutes(ctx) {
1157
1176
  return
1158
1177
  }
1159
1178
 
1179
+ if (route === 'style-save' && req.method === 'POST') {
1180
+ // 写作风格保存(0.1.19):style.md 整文件覆写(纯 markdown 基准
1181
+ // 文档,无行级键位,整文件替换即最小手术);base_hash 过期 → 409
1182
+ // 拒写(人工改动保护,与 project-save 同口径);text 非 string 422。
1183
+ const body = await readBody(req, res)
1184
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1185
+ json(res, 200, saveStyleText(novelDir, body.text, { baseHash: body.base_hash }))
1186
+ return
1187
+ }
1188
+
1189
+ if (route === 'chapter-save' && req.method === 'POST') {
1190
+ // 章节正文保存(0.1.19,面板章节阅读页编辑态):frontmatter 围栏
1191
+ // (含)之前逐字节保留、围栏后整段替换(正文是自由形态文本,无行级
1192
+ // 键位可手术);base_hash 过期 → 409 拒写(人工改动保护,与
1193
+ // project-save 同口径);body 非 string / 缺围栏 422、章节缺 404。
1194
+ const body = await readBody(req, res)
1195
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1196
+ json(res, 200, mutateChapterBody(novelDir, body.file, body.body, { baseHash: body.base_hash }))
1197
+ return
1198
+ }
1199
+
1200
+ if (route === 'outline-save' && req.method === 'POST') {
1201
+ // 章纲保存(0.1.19,章节阅读页章纲对照栏编辑态):章纲纯 markdown
1202
+ // 无 frontmatter,整文件替换;file 认章节文件名或 stem,目标恒为
1203
+ // outlines/<stem>.md(不存在 404,v1 不做面板新建章纲);base_hash
1204
+ // 检测同上;stem 不合 vXX-cXXX 规范 422。
1205
+ const body = await readBody(req, res)
1206
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1207
+ json(res, 200, mutateOutlineBody(novelDir, body.file, body.body, { baseHash: body.base_hash }))
1208
+ return
1209
+ }
1210
+
1211
+ if (route === 'file-source' && req.method === 'GET') {
1212
+ // 受管文件读(0.1.20「全量可编辑写面」,四条通用路由之一):rel 白
1213
+ // 名单校验 + 结构口径在 panel-core(managedRelCheck / readManagedFile),
1214
+ // 路由只做围栏与分发。hash 为原文指纹——编辑态保存的 base_hash 基线。
1215
+ const novelDir = resolveNovelRoot(url.searchParams.get('path'), ctx.workspaceRegistry)
1216
+ json(res, 200, readManagedFile(novelDir, url.searchParams.get('rel')))
1217
+ return
1218
+ }
1219
+
1220
+ if (route === 'file-save' && req.method === 'POST') {
1221
+ // 受管文件存:整文件覆写 + 保存时结构校验(解析失败/缺围栏 422)+
1222
+ // base_hash 过期 409(人工改动保护,与 project-save 同口径文案)。
1223
+ const body = await readBody(req, res)
1224
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1225
+ json(res, 200, saveManagedFile(novelDir, body.rel, body.text, { baseHash: body.base_hash }))
1226
+ return
1227
+ }
1228
+
1229
+ if (route === 'file-create' && req.method === 'POST') {
1230
+ // 受管文件建:已存在 409(绝不覆盖),结构校验同存(新角色卡/新章
1231
+ // 纲由客户端模板预填,服务端只验形状)。
1232
+ const body = await readBody(req, res)
1233
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1234
+ json(res, 200, createManagedFile(novelDir, body.rel, body.text))
1235
+ return
1236
+ }
1237
+
1238
+ if (route === 'file-delete' && req.method === 'POST') {
1239
+ // 受管文件删:不可逆,服务端不做二次确认(UI 层强制确认弹层);
1240
+ // 白名单最严——账本 yml / md 实体卡 / outlines 之外只有 chapters/
1241
+ // 的删除(正文编辑走 chapter-save,通用存不开放防绕过状态机语义)。
1242
+ const body = await readBody(req, res)
1243
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1244
+ json(res, 200, deleteManagedFile(novelDir, body.rel))
1245
+ return
1246
+ }
1247
+
1160
1248
  if (route === 'rebuild-index' && req.method === 'POST') {
1161
1249
  const body = await readBody(req, res)
1162
1250
  const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
package/lib/base.css CHANGED
@@ -116,12 +116,13 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
116
116
  .myt-closeBtn:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
117
117
  .myt-closeGlyph { font-size: 18px; line-height: 1; }
118
118
 
119
- /* 内容区:自身滚动容器 */
119
+ /* 内容区:自身滚动容器(顶部 16px 与 tab 条拉开距离,角色卡墙等
120
+ 首元素带边框时不贴条;左右 24 与 tab 条 padding 对齐)*/
120
121
  .myt-body {
121
122
  flex: 1 1 auto;
122
123
  min-height: 0;
123
124
  overflow-y: auto;
124
- padding: 0 24px 32px;
125
+ padding: 16px 24px 32px;
125
126
  }
126
127
  /* 宽度随宿主中栏自适应:不设上限(980 上限已去,宽屏撑满);min-width
127
128
  兜底极窄中栏,低于时 .myt-body(overflow-y:auto 连带 overflow-x:auto)
@@ -203,6 +204,42 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
203
204
  font-weight: 600;
204
205
  }
205
206
 
207
+ /* ── 设定 tab 内的子分区条(比主导航 tab 小一号)─────────────────────── */
208
+ .myt-subTabs {
209
+ flex: none;
210
+ display: flex;
211
+ align-items: center;
212
+ gap: 4px;
213
+ overflow-x: auto;
214
+ scrollbar-width: none;
215
+ }
216
+ .myt-subTab {
217
+ display: inline-flex;
218
+ align-items: center;
219
+ gap: 6px;
220
+ padding: 4px 10px;
221
+ border: none;
222
+ border-radius: 7px;
223
+ background: transparent;
224
+ color: var(--dsw-alias-label-secondary);
225
+ font: inherit;
226
+ font-size: 12.5px;
227
+ line-height: 18px;
228
+ cursor: pointer;
229
+ white-space: nowrap;
230
+ }
231
+ .myt-subTab:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
232
+ .myt-subTab[data-active="true"] {
233
+ background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 14%, transparent);
234
+ color: var(--dsw-alias-state-business-primary);
235
+ font-weight: 600;
236
+ }
237
+ .myt-subTab .myt-boardCount { line-height: 16px; }
238
+ /* 二级分类 tab(设定 tab 内 world 分区按 category 再切):比一级子 tab 再小一号 */
239
+ .myt-subTabsInner { margin-top: -2px; }
240
+ .myt-subTabsInner .myt-subTab { font-size: 12px; line-height: 16px; padding: 3px 8px; border-radius: 6px; }
241
+ .myt-subTabsInner .myt-subTab .myt-boardCount { line-height: 14px; }
242
+
206
243
  /* ── 卡片 / 栅格 ───────────────────────────────────────────────────────── */
207
244
  .myt-card {
208
245
  display: flex;
@@ -424,6 +461,59 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
424
461
  .myt-dimLabel { font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); margin-bottom: 2px; }
425
462
  .myt-dimText { font-size: 12px; line-height: 18px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; }
426
463
 
464
+ /* ── 0.1.19:正文/章纲面板编辑态(textarea + 手动保存)────────────────────
465
+ 与只读态的区分要一眼可辨:全宽等宽 textarea + 主题色边框强调;只读态是
466
+ 无边框淡底的 pre(.myt-chapterBody)与 MdView。 */
467
+ .myt-bodyEdit { display: flex; flex-direction: column; gap: 8px; }
468
+ .myt-chapterEditArea {
469
+ width: 100%;
470
+ box-sizing: border-box;
471
+ padding: 12px;
472
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 45%, transparent);
473
+ border-radius: 10px;
474
+ background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-layer-2));
475
+ color: var(--dsw-alias-label-primary);
476
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
477
+ font-size: 13px;
478
+ line-height: 1.7;
479
+ resize: vertical;
480
+ min-height: 320px;
481
+ }
482
+ .myt-editHint { font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); }
483
+ .myt-outlineEditBtn {
484
+ display: inline-flex;
485
+ align-items: center;
486
+ gap: 3px;
487
+ padding: 0 6px;
488
+ border-radius: 6px;
489
+ color: var(--dsw-alias-state-business-primary);
490
+ font-size: 11px;
491
+ line-height: 18px;
492
+ cursor: pointer;
493
+ user-select: none;
494
+ }
495
+ .myt-outlineEditBtn:hover { background: var(--dsw-alias-interactive-bg-hover); }
496
+ .myt-outlineEdit {
497
+ display: flex;
498
+ flex-direction: column;
499
+ gap: 8px;
500
+ padding: 10px 12px 12px;
501
+ border-top: 1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent);
502
+ }
503
+ .myt-outlineArea {
504
+ width: 100%;
505
+ box-sizing: border-box;
506
+ padding: 10px;
507
+ border: 1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 45%, transparent);
508
+ border-radius: 8px;
509
+ background: var(--dsw-alias-bg-layer-1, var(--dsw-alias-bg-layer-2));
510
+ color: var(--dsw-alias-label-primary);
511
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
512
+ font-size: 12px;
513
+ line-height: 1.6;
514
+ resize: vertical;
515
+ }
516
+
427
517
  /* ── 五期:章纲对照栏(章节抽屉)──────────────────────────────────────────
428
518
  与正文视觉区分:主题色左边线 + 淡主题色底,深色随令牌自动跟随。 */
429
519
  .myt-outlinePanel {
@@ -585,6 +675,17 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
585
675
  .myt-modalHead { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
586
676
  .myt-modalTitle { margin: 0; min-width: 0; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
587
677
  .myt-modalBody { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
678
+ /* ── 受管文件写面(0.2.x):源文件编辑/新建/删除确认三弹层 ────────────────
679
+ srcCard 比 modalCard 宽一档:整文件 textarea 是主角,宽度优先给代码文本。 */
680
+ .myt-srcCard { width: min(760px, 100%); }
681
+ .myt-srcRel { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; }
682
+ .myt-srcHint { font-size: 11px; line-height: 16px; }
683
+ .myt-srcArea { min-height: 46vh; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.6; }
684
+ .myt-itemActions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
685
+ .myt-itemActions .myt-btn { padding: 2px 8px; font-size: 11px; line-height: 16px; }
686
+ .myt-confirmCard { width: min(460px, 100%); }
687
+ .myt-confirmText { font-size: 12px; line-height: 19px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; white-space: pre-wrap; }
688
+ .myt-outlineDelBtn { color: var(--dsw-alias-state-error-primary); }
588
689
  .myt-kvRow { display: flex; gap: 10px; font-size: 12px; line-height: 18px; }
589
690
  .myt-kvKey { flex: none; width: 64px; color: var(--dsw-alias-label-tertiary); }
590
691
  .myt-kvVal { min-width: 0; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; white-space: pre-wrap; }
@@ -623,6 +724,42 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
623
724
  overflow-wrap: anywhere;
624
725
  }
625
726
  .myt-graphFoot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
727
+
728
+ /* ── 关系图角色图例:色点 + 角色名(canvas 内色同 NODE_ROLE_COLORS)──── */
729
+ .myt-graphLegend { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; }
730
+ .myt-legendItem { display: inline-flex; align-items: center; gap: 4px; }
731
+ .myt-legendDot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
732
+ .myt-legendDot[data-role="protagonist"] { background: #f53f3f; }
733
+ .myt-legendDot[data-role="supporting"] { background: #165dff; }
734
+ .myt-legendDot[data-role="antagonist"] { background: #1d2129; border: 1px solid var(--dsw-alias-border-l4, var(--dsw-alias-border-l2)); }
735
+ .myt-legendDot[data-role="minor"] { background: #fadc19; border: 1px solid color-mix(in srgb, var(--dsw-alias-label-tertiary) 60%, transparent); }
736
+
737
+ /* ── 关系图工具角标:+/- 缩放常驻;全屏按钮能力检测通过才渲染 ───────── */
738
+ .myt-graphTools { position: absolute; top: 10px; right: 10px; display: flex; gap: 6px; z-index: 3; }
739
+ .myt-graphTool {
740
+ display: inline-flex;
741
+ align-items: center;
742
+ justify-content: center;
743
+ width: 26px;
744
+ height: 26px;
745
+ padding: 0;
746
+ border: 1px solid var(--dsw-alias-border-l2);
747
+ border-radius: 7px;
748
+ background: var(--dsw-alias-bg-layer-2);
749
+ color: var(--dsw-alias-label-secondary);
750
+ font: inherit;
751
+ font-size: 14px;
752
+ line-height: 1;
753
+ cursor: pointer;
754
+ }
755
+ .myt-graphTool:hover { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
756
+ .myt-graphToolText { width: auto; padding: 0 10px; font-size: 12px; line-height: 18px; }
757
+ .myt-graphWrap:fullscreen,
758
+ .myt-graphWrap:-webkit-full-screen {
759
+ border: none;
760
+ border-radius: 0;
761
+ background: var(--dsw-alias-bg-base, var(--dsw-alias-bg-layer-1));
762
+ }
626
763
  .myt-unlinked { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
627
764
  .myt-unlinkedTag { padding: 1px 8px; border-radius: 999px; border: 1px dashed var(--dsw-alias-border-l2); color: var(--dsw-alias-label-tertiary); font-size: 11px; line-height: 16px; }
628
765
 
@@ -899,3 +1036,8 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
899
1036
  .myt-setArea { resize: vertical; }
900
1037
  .myt-setSelect { cursor: pointer; }
901
1038
  .myt-setActions { display: flex; justify-content: flex-end; gap: 8px; }
1039
+ /* 写作风格 tab:编辑器撑满卡宽 + 等宽字形(编辑的是 md 源文件,对照正文
1040
+ pre 的口径);空态卡居中引导(缺失时用默认基线创建)。 */
1041
+ .myt-styleArea { width: 100%; box-sizing: border-box; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; line-height: 1.6; }
1042
+ .myt-styleEmpty { display: flex; flex-direction: column; align-items: flex-start; gap: 10px; padding: 8px 0 4px; }
1043
+ .myt-styleEmpty p { margin: 0; max-width: 560px; }