dsh-novel-write-engine 0.2.10 → 0.2.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/lib/client.js CHANGED
@@ -10,7 +10,338 @@ window.__ModuleLoader__.load({
10
10
  const PANEL_ID = "novel-workshop";
11
11
  const PANEL_ROUTE = "/api/novel-write-engine/panel";
12
12
  /** User-visible panel/tutorial version; keep in sync with package.json. */
13
- const PANEL_VERSION = "0.2.10";
13
+ const PANEL_VERSION = "0.2.12";
14
+ // src/command-reference.ts
15
+ /** The five registered command entry points, including the umbrella command. */
16
+ const COMMAND_ENTRY = [
17
+ {
18
+ form: "/novel",
19
+ summary: "状态总览;不带参数等价于 /novel status。所有子命令都从这里进入。"
20
+ },
21
+ {
22
+ form: "/novel-next",
23
+ summary: "等价于 /novel next:下一步工具与理由。"
24
+ },
25
+ {
26
+ form: "/novel-stats",
27
+ summary: "等价于 /novel stats:章节、总字数、均分、伏笔与规则包。"
28
+ },
29
+ {
30
+ form: "/novel-export <kind>",
31
+ summary: "等价于 /novel export <kind>;kind 为 md、txt、epub、outline、worldbook 或 proposal。"
32
+ },
33
+ {
34
+ form: "/novel-admin [<op> <json>]",
35
+ summary: "等价于 /novel admin;不带参数时列出全部隐藏/运维操作。"
36
+ }
37
+ ];
38
+ /**
39
+ * Every named `/novel` form. The bare `/novel` default is listed in COMMAND_ENTRY,
40
+ * so the total here is 25 rather than 26.
41
+ */
42
+ const COMMAND_GROUPS = [
43
+ {
44
+ title: "状态与内容",
45
+ hint: "只读查询,随时可跑",
46
+ items: [
47
+ {
48
+ form: "/novel status",
49
+ summary: "进度、项目选择、轮次、字数、门控与下一步。"
50
+ },
51
+ {
52
+ form: "/novel next",
53
+ summary: "下一步该调哪个工具、为什么;卡住时先看它。"
54
+ },
55
+ {
56
+ form: "/novel stats",
57
+ summary: "章节数、总字数(归档 + 草稿)、均分、伏笔、覆盖率与规则包。"
58
+ },
59
+ {
60
+ form: "/novel outline",
61
+ summary: "蓝图、大纲覆盖率、当前章卡与世界观计数。"
62
+ },
63
+ {
64
+ form: "/novel guide",
65
+ summary: "六态引导与下一步;新项目、写作中、完结都能用。"
66
+ },
67
+ {
68
+ form: "/novel report",
69
+ summary: "全书报告:进度、质量、伏笔、节奏与警告。"
70
+ },
71
+ {
72
+ form: "/novel doctor",
73
+ summary: "项目体检:锁、备份、状态文档、正文与大纲漂移。"
74
+ },
75
+ {
76
+ form: "/novel search <关键词>",
77
+ summary: "搜索正文、摘要、大纲、世界观与台账;空关键词会报用法。"
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ title: "项目与会话",
83
+ hint: "只改查看对象或登记,不动正文",
84
+ items: [
85
+ {
86
+ form: "/novel projects",
87
+ summary: "列出登记项目;* 当前会话,P 面板默认,! 缺失/待修复。"
88
+ },
89
+ {
90
+ form: "/novel projects --prune",
91
+ summary: "只预览失效登记(missing / not-project / unreadable),不写盘。"
92
+ },
93
+ {
94
+ form: "/novel projects --prune --yes",
95
+ summary: "删除失效登记行,不删项目目录;--yes 不能单独使用。"
96
+ },
97
+ {
98
+ form: "/novel current",
99
+ summary: "当前会话绑定、选择来源和失效状态。"
100
+ },
101
+ {
102
+ form: "/novel use <路径|书名|.>",
103
+ summary: "切换当前会话;不改变 active;点号按 cwd 规则选唯一项目。"
104
+ },
105
+ {
106
+ form: "/novel unbind",
107
+ summary: "只清当前会话绑定;项目、登记和正文都不动。"
108
+ },
109
+ {
110
+ form: "/novel scan",
111
+ summary: "只读扫描 cwd、cwd/novel 和直接子目录;不递归、不跟随符号链接。"
112
+ },
113
+ {
114
+ form: "/novel scan --register",
115
+ summary: "只登记健康项目;不选择、不改 active、不绑会话。"
116
+ }
117
+ ]
118
+ },
119
+ {
120
+ title: "续写与导出",
121
+ hint: "resume 只读;export 写入 exports/",
122
+ items: [
123
+ {
124
+ form: "/novel resume",
125
+ summary: "输出可粘贴的续写指令;只读,与面板“复制续写指令”同源。"
126
+ },
127
+ {
128
+ form: "/novel export md",
129
+ summary: "正文 Markdown。"
130
+ },
131
+ {
132
+ form: "/novel export txt",
133
+ summary: "正文纯文本。"
134
+ },
135
+ {
136
+ form: "/novel export epub",
137
+ summary: "正文 EPUB 3。"
138
+ },
139
+ {
140
+ form: "/novel export outline",
141
+ summary: "大纲计划。"
142
+ },
143
+ {
144
+ form: "/novel export worldbook",
145
+ summary: "世界观设定集。"
146
+ },
147
+ {
148
+ form: "/novel export proposal",
149
+ summary: "书级提案。"
150
+ }
151
+ ]
152
+ },
153
+ {
154
+ title: "隐藏与运维",
155
+ hint: "JSON 必须是对象",
156
+ items: [{
157
+ form: "/novel admin",
158
+ summary: "列出全部 25 个隐藏/运维操作与示例。"
159
+ }, {
160
+ form: "/novel admin <op> <json>",
161
+ summary: "执行一个操作;参数同对应工具,JSON 外层的双引号要保留。"
162
+ }]
163
+ }
164
+ ];
165
+ /** Number of named `/novel` forms (the bare `/novel` default is separate). */
166
+ const COMMAND_FORM_COUNT = COMMAND_GROUPS.reduce((total, group) => total + group.items.length, 0);
167
+ /**
168
+ * Operations reachable through `/novel admin <op> <json>`. This list must stay
169
+ * byte-for-byte aligned with `src/commands/admin.ts`; tests enforce that.
170
+ */
171
+ const ADMIN_OPERATION_GROUPS = [
172
+ {
173
+ title: "写作、状态与恢复",
174
+ hint: "参数与同名工具一致",
175
+ items: [
176
+ {
177
+ name: "novel_plan_world",
178
+ summary: "提交 P1 世界计划"
179
+ },
180
+ {
181
+ name: "novel_plan_chapter",
182
+ summary: "提交 P2 章节计划"
183
+ },
184
+ {
185
+ name: "novel_word_count",
186
+ summary: "统计纯文本或项目字数"
187
+ },
188
+ {
189
+ name: "novel_amend",
190
+ summary: "修复状态文档"
191
+ },
192
+ {
193
+ name: "novel_rollback",
194
+ summary: "回退阶段"
195
+ },
196
+ {
197
+ name: "novel_diff",
198
+ summary: "对比文本或章稿"
199
+ },
200
+ {
201
+ name: "novel_audit",
202
+ summary: "审计 journal"
203
+ },
204
+ {
205
+ name: "novel_pipeline",
206
+ summary: "下一步建议"
207
+ },
208
+ {
209
+ name: "novel_state",
210
+ summary: "读取原始状态、历史与备份"
211
+ },
212
+ {
213
+ name: "novel_reopen",
214
+ summary: "重开已归档章"
215
+ },
216
+ {
217
+ name: "novel_restore",
218
+ summary: "从备份事务恢复"
219
+ }
220
+ ]
221
+ },
222
+ {
223
+ title: "规划、检索与诊断",
224
+ hint: "书级写操作也在这里",
225
+ items: [
226
+ {
227
+ name: "novel_blueprint",
228
+ summary: "写书级蓝图与卷"
229
+ },
230
+ {
231
+ name: "novel_outline",
232
+ summary: "章卡与卷 upsert"
233
+ },
234
+ {
235
+ name: "novel_outline_import",
236
+ summary: "导入 Markdown / YAML / JSON 大纲"
237
+ },
238
+ {
239
+ name: "novel_worldbook",
240
+ summary: "按段更新世界观"
241
+ },
242
+ {
243
+ name: "novel_search",
244
+ summary: "搜索正文、大纲与台账"
245
+ },
246
+ {
247
+ name: "novel_doctor",
248
+ summary: "项目体检"
249
+ },
250
+ {
251
+ name: "novel_report",
252
+ summary: "全书报告"
253
+ },
254
+ {
255
+ name: "novel_guide",
256
+ summary: "引导与下一步"
257
+ }
258
+ ]
259
+ },
260
+ {
261
+ title: "导出",
262
+ hint: "六种 kind 同一入口",
263
+ items: [{
264
+ name: "novel_export",
265
+ summary: "导出正文、大纲、世界观或提案"
266
+ }]
267
+ },
268
+ {
269
+ title: "登记与设定包",
270
+ hint: "只走命令入口,不注册工具",
271
+ items: [
272
+ {
273
+ name: "novel_registry_list",
274
+ summary: "列出登记项目"
275
+ },
276
+ {
277
+ name: "novel_registry_register",
278
+ summary: "登记一个项目,传 root"
279
+ },
280
+ {
281
+ name: "novel_registry_activate",
282
+ summary: "改面板默认 active,传 root"
283
+ },
284
+ {
285
+ name: "novel_registry_forget",
286
+ summary: "删登记行,传 root"
287
+ },
288
+ {
289
+ name: "novel_import",
290
+ summary: "导入设定包"
291
+ }
292
+ ]
293
+ }
294
+ ];
295
+ const ADMIN_OPERATION_COUNT = ADMIN_OPERATION_GROUPS.flatMap((group) => group.items.map((operation) => operation.name)).length;
296
+ /** Copyable workflows; no hidden state changes beyond the listed commands. */
297
+ const COMMAND_COMBINATIONS = [
298
+ {
299
+ title: "扫描、登记、切换",
300
+ steps: [
301
+ "/novel scan",
302
+ "/novel scan --register",
303
+ "/novel projects",
304
+ "/novel use <路径|书名|.>",
305
+ "/novel resume"
306
+ ],
307
+ note: "scan 只发现,--register 只登记,use 才切换当前会话;resume 最后生成续写指令。"
308
+ },
309
+ {
310
+ title: "清理失效登记",
311
+ steps: ["/novel projects --prune", "/novel projects --prune --yes"],
312
+ note: "第一条只预览;第二条才删除登记行,项目目录不会被删。"
313
+ },
314
+ {
315
+ title: "接续当前这本书",
316
+ steps: [
317
+ "/novel current",
318
+ "/novel status",
319
+ "/novel resume"
320
+ ],
321
+ note: "current 确认绑定,status 确认阶段,resume 给 Agent 一段可粘贴的续写指令。"
322
+ },
323
+ {
324
+ title: "core 模式调用隐藏工具",
325
+ steps: [
326
+ "/novel admin",
327
+ "/novel admin novel_state {\"kind\":\"pipeline\"}",
328
+ "/novel admin novel_word_count {\"text\":\"一段正文\"}"
329
+ ],
330
+ note: "JSON 必须是对象;命令里的双引号要保留。toolsMode=core 时,18 个非核心工具都走这里。"
331
+ },
332
+ {
333
+ title: "导出与排错",
334
+ steps: [
335
+ "/novel export md",
336
+ "/novel export outline",
337
+ "/novel export worldbook",
338
+ "/novel status",
339
+ "/novel next",
340
+ "/novel doctor"
341
+ ],
342
+ note: "导出只写 exports/;排错先看 status、next、doctor,再决定 amend、restore 或 rollback。"
343
+ }
344
+ ];
14
345
  // src/next-reasons.ts
15
346
  /**
16
347
  * Chinese one-line explanation for the tool each engine state recommends.
@@ -495,11 +826,12 @@ window.__ModuleLoader__.load({
495
826
  width: 100%;
496
827
  max-width: 1120px;
497
828
  height: 100%;
829
+ max-height: 100%;
498
830
  min-height: 0;
499
831
  overflow-x: hidden;
500
832
  overflow-y: auto;
501
833
  overscroll-behavior: contain;
502
- scrollbar-gutter: stable;
834
+ scrollbar-width: none;
503
835
  margin: 0 auto;
504
836
  padding: clamp(18px, 3.2vw, 38px);
505
837
  color: var(--nwe-ink);
@@ -507,6 +839,7 @@ window.__ModuleLoader__.load({
507
839
  line-height: 1.55;
508
840
  }
509
841
  .nwe-root * { box-sizing: border-box; }
842
+ .nwe-root::-webkit-scrollbar { width: 0; height: 0; display: none; }
510
843
  .nwe-root::before {
511
844
  content: "";
512
845
  position: absolute;
@@ -615,6 +948,7 @@ window.__ModuleLoader__.load({
615
948
  }
616
949
  .nwe-help:hover { border-color: color-mix(in srgb, var(--nwe-gold) 60%, var(--nwe-border)); color: var(--nwe-ink); transform: translateY(-1px); }
617
950
  .nwe-help svg { flex: 0 0 auto; }
951
+ .nwe-root:focus-visible { outline: 2px solid var(--nwe-gold); outline-offset: -3px; }
618
952
  .nwe-root :focus-visible { outline: 2px solid var(--nwe-gold); outline-offset: 3px; }
619
953
  .nwe-modal-backdrop {
620
954
  position: fixed;
@@ -699,6 +1033,27 @@ window.__ModuleLoader__.load({
699
1033
  .nwe-step p strong { color: var(--nwe-ink); font-weight: 600; }
700
1034
  .nwe-tutorial-note { margin: 12px 0 0; padding: 10px 12px; border-radius: 9px; background: color-mix(in srgb, var(--nwe-gold) 8%, transparent); color: var(--nwe-muted); font-size: 12px; line-height: 1.7; }
701
1035
  .nwe-tutorial-note strong { color: var(--nwe-gold); }
1036
+ .nwe-command-groups { display: grid; gap: 9px; margin-top: 10px; }
1037
+ .nwe-command-group { overflow: hidden; border: 1px solid var(--nwe-border); border-radius: 11px; background: color-mix(in srgb, var(--nwe-surface) 88%, var(--nwe-raised)); }
1038
+ .nwe-command-group-head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; padding: 9px 12px; border-bottom: 1px solid var(--nwe-border); background: color-mix(in srgb, var(--nwe-raised) 45%, transparent); }
1039
+ .nwe-command-group-head h4 { margin: 0; font-size: 12px; font-weight: 650; letter-spacing: .02em; }
1040
+ .nwe-command-group-head span { color: var(--nwe-muted); font-size: 12px; text-align: right; }
1041
+ .nwe-command-rows { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1px; background: var(--nwe-border); }
1042
+ .nwe-command-row { min-width: 0; padding: 9px 11px; background: var(--nwe-surface); }
1043
+ .nwe-command-form { display: block; color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.5; overflow-wrap: anywhere; }
1044
+ .nwe-command-desc { display: block; margin-top: 3px; color: var(--nwe-muted); font-size: 12px; line-height: 1.55; }
1045
+ .nwe-command-subhead { margin: 17px 0 0; font-size: 12px; font-weight: 650; letter-spacing: .02em; }
1046
+ .nwe-admin-groups { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 9px; }
1047
+ .nwe-admin-group { padding: 10px 11px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, var(--nwe-raised) 42%, transparent); }
1048
+ .nwe-admin-group h5 { margin: 0; color: var(--nwe-ink); font-size: 12px; }
1049
+ .nwe-admin-group p { margin: 3px 0 7px; color: var(--nwe-muted); font-size: 12px; line-height: 1.5; }
1050
+ .nwe-admin-list { display: flex; flex-wrap: wrap; gap: 5px; margin: 0; padding: 0; list-style: none; }
1051
+ .nwe-admin-list code { padding: 2px 5px; border: 1px solid var(--nwe-border); border-radius: 5px; color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 11px; overflow-wrap: anywhere; }
1052
+ .nwe-command-combos { display: grid; gap: 7px; margin: 9px 0 0; padding: 0; list-style: none; }
1053
+ .nwe-command-combos li { padding: 10px 12px; border: 1px solid var(--nwe-border); border-radius: 10px; background: color-mix(in srgb, var(--nwe-surface) 86%, var(--nwe-raised)); }
1054
+ .nwe-command-combos strong { display: block; color: var(--nwe-ink); font-size: 12px; }
1055
+ .nwe-command-steps { display: block; margin: 4px 0 0; padding: 7px 9px; border: 1px solid var(--nwe-border); border-radius: 7px; background: color-mix(in srgb, #000 16%, var(--nwe-surface)); color: var(--nwe-gold); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; line-height: 1.7; white-space: pre-wrap; overflow-wrap: anywhere; }
1056
+ .nwe-command-combos p { margin: 5px 0 0; color: var(--nwe-muted); font-size: 12px; line-height: 1.55; }
702
1057
  .nwe-faq { display: flex; flex-direction: column; gap: 8px; margin: 0; padding: 0; list-style: none; }
703
1058
  .nwe-faq li { padding: 10px 12px; border: 1px solid var(--nwe-border); border-radius: 10px; }
704
1059
  .nwe-faq strong { display: block; color: var(--nwe-ink); font-size: 12px; }
@@ -957,6 +1312,9 @@ window.__ModuleLoader__.load({
957
1312
  .nwe-modal-body { padding: 16px 18px 22px; }
958
1313
  .nwe-modal-foot { padding: 9px 18px; }
959
1314
  .nwe-tutorial-cards { grid-template-columns: 1fr; }
1315
+ .nwe-command-rows, .nwe-admin-groups { grid-template-columns: 1fr; }
1316
+ .nwe-command-group-head { align-items: flex-start; flex-direction: column; gap: 2px; }
1317
+ .nwe-command-group-head span { text-align: left; }
960
1318
  .nwe-step-tool { margin-left: 0; }
961
1319
  .nwe-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
962
1320
  .nwe-book-grid { grid-template-columns: 1fr; }
@@ -1537,8 +1895,6 @@ window.__ModuleLoader__.load({
1537
1895
  font-weight: 650;
1538
1896
  }
1539
1897
  .nwe-dossier > summary::-webkit-details-marker { display: none; }
1540
- .nwe-dossier > summary::before { content: "+"; color: var(--nwe-accent); font-size: 16px; line-height: 1; }
1541
- .nwe-dossier[open] > summary::before { content: "−"; }
1542
1898
  .nwe-dossier-note { margin-left: auto; color: var(--nwe-muted); font-size: 11px; font-weight: 400; }
1543
1899
  .nwe-dossier .nwe-section { margin-top: 12px; }
1544
1900
  .nwe-dossier .nwe-section-head { display: none; }
@@ -1675,7 +2031,17 @@ window.__ModuleLoader__.load({
1675
2031
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
1676
2032
  gap: 10px;
1677
2033
  align-items: start;
2034
+ max-height: min(56vh, 580px);
2035
+ overflow-x: hidden;
2036
+ overflow-y: auto;
2037
+ overscroll-behavior: contain;
2038
+ scrollbar-gutter: stable;
1678
2039
  margin-top: 9px;
2040
+ padding-right: 4px;
2041
+ }
2042
+ .nwe-card-grid:focus-visible {
2043
+ outline: 2px solid var(--nwe-accent);
2044
+ outline-offset: -2px;
1679
2045
  }
1680
2046
  .nwe-data-card {
1681
2047
  overflow: hidden;
@@ -1758,9 +2124,17 @@ window.__ModuleLoader__.load({
1758
2124
  .nwe-worldbook-layout > .nwe-subsection:first-child,
1759
2125
  .nwe-leads-layout > .nwe-subsection:first-child { grid-column: 1 / -1; }
1760
2126
  .nwe-worldbook-layout > .nwe-subsection .nwe-card-grid,
1761
- .nwe-leads-layout > .nwe-subsection .nwe-card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2127
+ .nwe-leads-layout > .nwe-subsection .nwe-card-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); max-height: min(40vh, 400px); }
1762
2128
  .nwe-worldbook-layout > .nwe-subsection:first-child .nwe-card-grid,
1763
2129
  .nwe-leads-layout > .nwe-subsection:first-child .nwe-card-grid { grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); }
2130
+ .nwe-worldbook-layout > .nwe-subsection:has(.nwe-data-card[open]),
2131
+ .nwe-leads-layout > .nwe-subsection:has(.nwe-data-card[open]) { grid-column: 1 / -1; }
2132
+ .nwe-worldbook-layout > .nwe-subsection:has(.nwe-data-card[open]) .nwe-card-grid,
2133
+ .nwe-leads-layout > .nwe-subsection:has(.nwe-data-card[open]) .nwe-card-grid {
2134
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
2135
+ max-height: min(52vh, 540px);
2136
+ }
2137
+ #nwe-panel-characters .nwe-card-grid { max-height: min(60vh, 620px); }
1764
2138
  .nwe-quality-layout { display: block; }
1765
2139
  .nwe-quality-kpis {
1766
2140
  display: grid;
@@ -1887,6 +2261,323 @@ window.__ModuleLoader__.load({
1887
2261
  .nwe-chapters-section .nwe-table-wrap { display: none; }
1888
2262
  .nwe-chapter-cards-mobile { display: grid; gap: 8px; margin-top: 10px; }
1889
2263
  }
2264
+
2265
+ /* v10 data surfaces: the host clips its fixed centre column, so the page owns
2266
+ exactly one scroller and the chrome stays in flow. The root scrollbar is
2267
+ hidden; card lists and detail bodies below keep their visible local bars. */
2268
+ .nwe-root {
2269
+ display: block;
2270
+ flex: 1 1 auto;
2271
+ height: 100%;
2272
+ max-height: 100%;
2273
+ min-height: 0;
2274
+ overflow-x: hidden;
2275
+ overflow-y: auto;
2276
+ overscroll-behavior: contain;
2277
+ scrollbar-width: none;
2278
+ }
2279
+ .nwe-root::-webkit-scrollbar { width: 0; height: 0; display: none; }
2280
+ .nwe-workbench {
2281
+ min-width: 0;
2282
+ margin-top: 2px;
2283
+ padding: 0 6px 2px 0;
2284
+ overflow: visible;
2285
+ }
2286
+ .nwe-root details > summary { cursor: pointer; list-style: none; }
2287
+ .nwe-root details > summary::-webkit-details-marker { display: none; }
2288
+ .nwe-root details > summary::marker { content: ""; }
2289
+ .nwe-root details > summary:focus-visible { outline: 2px solid var(--nwe-accent); outline-offset: -2px; }
2290
+
2291
+ .nwe-data-card[open] {
2292
+ grid-column: 1 / -1;
2293
+ background: var(--nwe-surface);
2294
+ box-shadow: 0 14px 34px -28px color-mix(in srgb, #000 62%, transparent);
2295
+ }
2296
+ .nwe-data-card-head { align-items: center; transition: background .16s ease; }
2297
+ .nwe-data-card:hover .nwe-data-card-head { background: color-mix(in srgb, var(--nwe-accent) 4%, transparent); }
2298
+ .nwe-data-card-body {
2299
+ max-height: min(42vh, 360px);
2300
+ overflow: auto;
2301
+ overscroll-behavior: contain;
2302
+ scrollbar-gutter: stable;
2303
+ }
2304
+ .nwe-data-card-body:focus-visible,
2305
+ .nwe-info-card-body:focus-visible,
2306
+ .nwe-dossier-body:focus-visible,
2307
+ .nwe-fold-body:focus-visible,
2308
+ .nwe-card-grid:focus-visible,
2309
+ .nwe-table-wrap:focus-visible {
2310
+ outline: 2px solid var(--nwe-accent);
2311
+ outline-offset: -2px;
2312
+ }
2313
+ .nwe-data-card-body::-webkit-scrollbar,
2314
+ .nwe-info-card-body::-webkit-scrollbar,
2315
+ .nwe-dossier-body::-webkit-scrollbar,
2316
+ .nwe-fold-body::-webkit-scrollbar,
2317
+ .nwe-card-grid::-webkit-scrollbar,
2318
+ .nwe-table-wrap::-webkit-scrollbar { width: 10px; height: 10px; }
2319
+ .nwe-data-card-body::-webkit-scrollbar-track,
2320
+ .nwe-info-card-body::-webkit-scrollbar-track,
2321
+ .nwe-dossier-body::-webkit-scrollbar-track,
2322
+ .nwe-fold-body::-webkit-scrollbar-track,
2323
+ .nwe-card-grid::-webkit-scrollbar-track,
2324
+ .nwe-table-wrap::-webkit-scrollbar-track {
2325
+ border-radius: 999px;
2326
+ background: color-mix(in srgb, var(--nwe-ink) 5%, transparent);
2327
+ }
2328
+ .nwe-data-card-body::-webkit-scrollbar-button,
2329
+ .nwe-info-card-body::-webkit-scrollbar-button,
2330
+ .nwe-dossier-body::-webkit-scrollbar-button,
2331
+ .nwe-fold-body::-webkit-scrollbar-button,
2332
+ .nwe-card-grid::-webkit-scrollbar-button,
2333
+ .nwe-table-wrap::-webkit-scrollbar-button { display: none; }
2334
+ .nwe-data-card-body::-webkit-scrollbar-thumb,
2335
+ .nwe-info-card-body::-webkit-scrollbar-thumb,
2336
+ .nwe-dossier-body::-webkit-scrollbar-thumb,
2337
+ .nwe-fold-body::-webkit-scrollbar-thumb,
2338
+ .nwe-card-grid::-webkit-scrollbar-thumb,
2339
+ .nwe-table-wrap::-webkit-scrollbar-thumb {
2340
+ border: 2px solid transparent;
2341
+ border-radius: 999px;
2342
+ background: color-mix(in srgb, var(--nwe-ink) 28%, transparent);
2343
+ background-clip: padding-box;
2344
+ }
2345
+ .nwe-data-card-body::-webkit-scrollbar-thumb:hover,
2346
+ .nwe-info-card-body::-webkit-scrollbar-thumb:hover,
2347
+ .nwe-dossier-body::-webkit-scrollbar-thumb:hover,
2348
+ .nwe-fold-body::-webkit-scrollbar-thumb:hover,
2349
+ .nwe-card-grid::-webkit-scrollbar-thumb:hover,
2350
+ .nwe-table-wrap::-webkit-scrollbar-thumb:hover {
2351
+ background: color-mix(in srgb, var(--nwe-ink) 42%, transparent);
2352
+ background-clip: padding-box;
2353
+ }
2354
+
2355
+ .nwe-info-grid {
2356
+ display: grid;
2357
+ grid-template-columns: repeat(3, minmax(0, 1fr));
2358
+ gap: 8px;
2359
+ border: 0;
2360
+ border-radius: 0;
2361
+ background: transparent;
2362
+ }
2363
+ .nwe-info-card {
2364
+ min-width: 0;
2365
+ overflow: hidden;
2366
+ border: 1px solid var(--nwe-border);
2367
+ border-radius: 8px;
2368
+ background: var(--nwe-surface);
2369
+ transition: border-color .16s ease, background .16s ease, box-shadow .16s ease;
2370
+ }
2371
+ .nwe-info-card:hover {
2372
+ border-color: color-mix(in srgb, var(--nwe-accent) 30%, var(--nwe-border));
2373
+ background: color-mix(in srgb, var(--nwe-accent) 3%, var(--nwe-surface));
2374
+ }
2375
+ .nwe-info-card[open] {
2376
+ grid-column: 1 / -1;
2377
+ border-color: color-mix(in srgb, var(--nwe-accent) 36%, var(--nwe-border));
2378
+ background: var(--nwe-surface);
2379
+ box-shadow: 0 14px 34px -30px color-mix(in srgb, #000 62%, transparent);
2380
+ }
2381
+ .nwe-info-card-head {
2382
+ display: grid;
2383
+ grid-template-columns: 30px minmax(0, 1fr) 14px;
2384
+ gap: 10px;
2385
+ align-items: center;
2386
+ padding: 13px 14px;
2387
+ }
2388
+ .nwe-info-card-icon {
2389
+ display: grid;
2390
+ place-items: center;
2391
+ width: 30px;
2392
+ height: 30px;
2393
+ border-radius: 8px;
2394
+ background: var(--nwe-accent-soft);
2395
+ color: var(--nwe-accent);
2396
+ }
2397
+ .nwe-info-card-title { overflow-wrap: anywhere; font-size: 12.5px; font-weight: 650; line-height: 1.4; }
2398
+ .nwe-info-card-caret {
2399
+ color: var(--nwe-muted);
2400
+ font-size: 16px;
2401
+ line-height: 1;
2402
+ transition: transform .16s ease, color .16s ease;
2403
+ }
2404
+ .nwe-info-card[open] .nwe-info-card-caret { transform: rotate(45deg); color: var(--nwe-accent); }
2405
+ .nwe-info-card-body {
2406
+ max-height: min(48vh, 460px);
2407
+ overflow: auto;
2408
+ overscroll-behavior: contain;
2409
+ scrollbar-gutter: stable;
2410
+ padding: 12px 14px 14px;
2411
+ border-top: 1px solid var(--nwe-border);
2412
+ background: color-mix(in srgb, var(--nwe-raised) 34%, transparent);
2413
+ }
2414
+ .nwe-info-card-body > :first-child { margin-top: 0; }
2415
+ .nwe-info-card-body .nwe-v6-list { margin-top: 0; gap: 7px; }
2416
+ .nwe-info-card-body .nwe-v6-list li { grid-template-columns: minmax(6.5em, max-content) minmax(0, 1fr); }
2417
+ .nwe-info-card-body .nwe-v6-list b { font-weight: 600; }
2418
+ .nwe-info-card-body .nwe-v6-bars { margin-top: 12px; }
2419
+
2420
+ .nwe-dossier {
2421
+ flex: 0 0 auto;
2422
+ margin-top: 28px;
2423
+ padding-top: 10px;
2424
+ border-top: 1px solid var(--nwe-border);
2425
+ }
2426
+ .nwe-dossier > summary {
2427
+ display: grid;
2428
+ grid-template-columns: 30px minmax(0, 1fr) 14px;
2429
+ gap: 10px;
2430
+ align-items: center;
2431
+ padding: 8px 0;
2432
+ }
2433
+ .nwe-dossier > summary::before { content: none; }
2434
+ .nwe-dossier-icon {
2435
+ display: grid;
2436
+ place-items: center;
2437
+ width: 30px;
2438
+ height: 30px;
2439
+ border: 1px solid var(--nwe-border);
2440
+ border-radius: 8px;
2441
+ background: var(--nwe-surface);
2442
+ color: var(--nwe-accent);
2443
+ }
2444
+ .nwe-dossier-copy { min-width: 0; }
2445
+ .nwe-dossier-title { display: block; font-size: 14px; font-weight: 650; line-height: 1.35; }
2446
+ .nwe-dossier-note { display: block; margin: 2px 0 0; color: var(--nwe-muted); font-size: 11px; font-weight: 400; }
2447
+ .nwe-dossier-caret {
2448
+ color: var(--nwe-muted);
2449
+ font-size: 16px;
2450
+ line-height: 1;
2451
+ transition: transform .16s ease, color .16s ease;
2452
+ }
2453
+ .nwe-dossier[open] .nwe-dossier-caret { transform: rotate(45deg); color: var(--nwe-accent); }
2454
+ .nwe-dossier-body {
2455
+ max-height: min(50vh, 500px);
2456
+ overflow: auto;
2457
+ overscroll-behavior: contain;
2458
+ scrollbar-gutter: stable;
2459
+ margin-top: 8px;
2460
+ padding-right: 4px;
2461
+ }
2462
+ .nwe-dossier .nwe-section { margin-top: 12px; }
2463
+ .nwe-dossier .nwe-section:first-child { margin-top: 0; }
2464
+
2465
+ .nwe-fold {
2466
+ overflow: hidden;
2467
+ border: 1px solid var(--nwe-border);
2468
+ border-radius: 8px;
2469
+ background: color-mix(in srgb, var(--nwe-surface) 86%, var(--nwe-raised));
2470
+ transition: border-color .16s ease, background .16s ease;
2471
+ }
2472
+ .nwe-fold + .nwe-fold { margin-top: 8px; }
2473
+ .nwe-fold:hover { border-color: color-mix(in srgb, var(--nwe-accent) 28%, var(--nwe-border)); }
2474
+ .nwe-fold[open] { border-color: color-mix(in srgb, var(--nwe-accent) 34%, var(--nwe-border)); background: var(--nwe-surface); }
2475
+ .nwe-fold-head {
2476
+ display: grid;
2477
+ grid-template-columns: minmax(0, 1fr) auto 14px;
2478
+ gap: 8px;
2479
+ align-items: center;
2480
+ padding: 10px 12px;
2481
+ }
2482
+ .nwe-fold-title { overflow-wrap: anywhere; color: var(--nwe-ink); font-size: 12px; font-weight: 650; line-height: 1.45; }
2483
+ .nwe-fold-meta { color: var(--nwe-muted); font-size: 10.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
2484
+ .nwe-fold-caret {
2485
+ color: var(--nwe-muted);
2486
+ font-size: 15px;
2487
+ line-height: 1;
2488
+ transition: transform .16s ease, color .16s ease;
2489
+ }
2490
+ .nwe-fold[open] .nwe-fold-caret { transform: rotate(45deg); color: var(--nwe-accent); }
2491
+ .nwe-fold-body {
2492
+ max-height: min(46vh, 420px);
2493
+ overflow: auto;
2494
+ overscroll-behavior: contain;
2495
+ scrollbar-gutter: stable;
2496
+ padding: 10px 12px 12px;
2497
+ border-top: 1px solid var(--nwe-border);
2498
+ background: color-mix(in srgb, var(--nwe-raised) 28%, transparent);
2499
+ }
2500
+ .nwe-fold-body > :first-child { margin-top: 0; }
2501
+ .nwe-fold-body .nwe-detail-text { max-height: none; overflow: visible; margin: 0; padding: 0; border: 0; background: transparent; }
2502
+ .nwe-detail-fold .nwe-fold-body { max-height: min(38vh, 340px); }
2503
+
2504
+ .nwe-chapter[open] {
2505
+ border-color: color-mix(in srgb, var(--nwe-accent) 34%, var(--nwe-border));
2506
+ background: var(--nwe-surface);
2507
+ }
2508
+ .nwe-chapter[open] summary { background: color-mix(in srgb, var(--nwe-accent) 4%, transparent); }
2509
+ .nwe-chapter-detail {
2510
+ max-height: min(36vh, 300px);
2511
+ overflow: auto;
2512
+ overscroll-behavior: contain;
2513
+ margin: 0 4px 12px 64px;
2514
+ padding: 10px 12px;
2515
+ border: 1px solid var(--nwe-border);
2516
+ border-radius: 8px;
2517
+ background: color-mix(in srgb, var(--nwe-raised) 30%, transparent);
2518
+ color: var(--nwe-muted);
2519
+ font-size: 12px;
2520
+ line-height: 1.7;
2521
+ white-space: pre-wrap;
2522
+ overflow-wrap: anywhere;
2523
+ }
2524
+ .nwe-chapter-detail-panel {
2525
+ max-height: none;
2526
+ overflow: visible;
2527
+ margin: 12px 4px 0 64px;
2528
+ padding: 12px 14px;
2529
+ white-space: normal;
2530
+ }
2531
+ .nwe-chapter-detail-panel .nwe-section-head { margin-bottom: 10px; }
2532
+
2533
+ .nwe-table-wrap {
2534
+ max-height: min(58vh, 560px);
2535
+ overflow: auto;
2536
+ overscroll-behavior: contain;
2537
+ scrollbar-gutter: stable;
2538
+ }
2539
+ .nwe-chapter-table thead th { position: sticky; top: 0; z-index: 2; background: var(--nwe-surface); }
2540
+ .nwe-chapters { max-height: min(56vh, 460px); overflow-y: auto; overscroll-behavior: contain; scrollbar-gutter: stable; }
2541
+ .nwe-activity { max-height: 240px; overflow-y: auto; overscroll-behavior: contain; }
2542
+
2543
+ @media (min-width: 900px) {
2544
+ .nwe-data-card[open] .nwe-kv {
2545
+ grid-template-columns: minmax(76px, max-content) minmax(0, 1fr) minmax(76px, max-content) minmax(0, 1fr);
2546
+ column-gap: 14px;
2547
+ }
2548
+ }
2549
+ @media (max-width: 980px) {
2550
+ .nwe-info-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
2551
+ }
2552
+ @media (max-width: 760px) {
2553
+ .nwe-workbench { padding-right: 2px; }
2554
+ .nwe-info-grid { grid-template-columns: 1fr; }
2555
+ .nwe-info-card[open] { grid-column: auto; }
2556
+ .nwe-info-card-body { max-height: min(44vh, 420px); }
2557
+ .nwe-dossier > summary { grid-template-columns: 26px minmax(0, 1fr) 14px; }
2558
+ .nwe-dossier-icon { width: 26px; height: 26px; }
2559
+ .nwe-dossier-caret { grid-column: 3; }
2560
+ .nwe-dossier-note { display: none; }
2561
+ .nwe-chapter-detail { margin: 0 2px 10px; padding: 10px 11px; }
2562
+ .nwe-chapter-detail-panel { margin: 10px 2px 0; }
2563
+ .nwe-data-card[open] .nwe-kv { grid-template-columns: 78px minmax(0, 1fr); }
2564
+ .nwe-table-wrap { max-height: 62vh; }
2565
+ .nwe-chapters { max-height: 58vh; }
2566
+ }
2567
+
2568
+ /* A card body is a reading window, not a wheel trap: at its edge the page
2569
+ continues to scroll. The modal keeps contain so the page never moves behind it. */
2570
+ .nwe-card-grid,
2571
+ .nwe-data-card-body,
2572
+ .nwe-info-card-body,
2573
+ .nwe-dossier-body,
2574
+ .nwe-fold-body,
2575
+ .nwe-chapter-detail,
2576
+ .nwe-table-wrap,
2577
+ .nwe-chapters,
2578
+ .nwe-activity,
2579
+ .nwe-volumes,
2580
+ .nwe-desk-main .nwe-chapters { overscroll-behavior: auto; }
1890
2581
  `;
1891
2582
  const icon = (size, path) => (0, react.createElement)("svg", {
1892
2583
  width: size,
@@ -1938,6 +2629,20 @@ window.__ModuleLoader__.load({
1938
2629
  alert: "M12 4 3 20h18L12 4Zm0 5v5m0 3h.01"
1939
2630
  };
1940
2631
  const sectionGlyph = (name, size = 18) => icon(size, GLYPH_PATHS[name]);
2632
+ /** One disclosure language for every nested detail panel. */
2633
+ const foldBlock = (key, title, body, options = {}) => (0, react.createElement)("details", {
2634
+ className: `nwe-fold${options.className === void 0 ? "" : ` ${options.className}`}`,
2635
+ key,
2636
+ ...options.open === true ? { open: true } : {}
2637
+ }, (0, react.createElement)("summary", { className: "nwe-fold-head" }, (0, react.createElement)("span", { className: "nwe-fold-title" }, title), options.meta === void 0 ? null : (0, react.createElement)("span", { className: "nwe-fold-meta" }, options.meta), (0, react.createElement)("span", {
2638
+ className: "nwe-fold-caret",
2639
+ "aria-hidden": "true"
2640
+ }, "+")), body === null ? null : (0, react.createElement)("div", {
2641
+ className: `nwe-fold-body${options.bodyClassName === void 0 ? "" : ` ${options.bodyClassName}`}`,
2642
+ role: "region",
2643
+ "aria-label": `${title} 内容`,
2644
+ tabIndex: 0
2645
+ }, body));
1941
2646
  const TUTORIAL_STEPS = [
1942
2647
  {
1943
2648
  title: "建一本书的项目",
@@ -2319,6 +3024,23 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2319
3024
  "aria-live": "polite"
2320
3025
  }, status)));
2321
3026
  };
3027
+ const commandReferenceSection = () => {
3028
+ const groups = [{
3029
+ title: "入口命令",
3030
+ hint: "5 个:/novel 与 4 个短别名",
3031
+ items: COMMAND_ENTRY
3032
+ }, ...COMMAND_GROUPS];
3033
+ return (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "全部命令:入口、子命令与组合"), (0, react.createElement)("p", null, `5 个入口命令、/novel 的 ${COMMAND_FORM_COUNT} 种带名写法、${ADMIN_OPERATION_COUNT} 个 admin 操作。短命令只是别名;斜杠命令由你在输入框执行,Agent 不能代跑。`), (0, react.createElement)("div", { className: "nwe-command-groups" }, ...groups.map((group) => (0, react.createElement)("div", {
3034
+ className: "nwe-command-group",
3035
+ key: group.title
3036
+ }, (0, react.createElement)("div", { className: "nwe-command-group-head" }, (0, react.createElement)("h4", null, group.title), (0, react.createElement)("span", null, group.hint)), (0, react.createElement)("div", { className: "nwe-command-rows" }, ...group.items.map((item) => (0, react.createElement)("div", {
3037
+ className: "nwe-command-row",
3038
+ key: item.form
3039
+ }, (0, react.createElement)("code", { className: "nwe-command-form" }, item.form), (0, react.createElement)("span", { className: "nwe-command-desc" }, item.summary))))))), (0, react.createElement)("h4", { className: "nwe-command-subhead" }, `${ADMIN_OPERATION_COUNT} 个 admin 操作(/novel admin <op> <json>)`), (0, react.createElement)("div", { className: "nwe-admin-groups" }, ...ADMIN_OPERATION_GROUPS.map((group) => (0, react.createElement)("div", {
3040
+ className: "nwe-admin-group",
3041
+ key: group.title
3042
+ }, (0, react.createElement)("h5", null, group.title), (0, react.createElement)("p", null, group.hint), (0, react.createElement)("ul", { className: "nwe-admin-list" }, ...group.items.map((operation) => (0, react.createElement)("li", { key: operation.name }, (0, react.createElement)("code", { title: operation.summary }, operation.name))))))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "admin 示例:"), (0, react.createElement)("code", null, "/novel admin novel_state {\"kind\":\"pipeline\"}"), "、", (0, react.createElement)("code", null, "/novel admin novel_registry_register {\"root\":\"/abs/book\"}"), "。参数与同名工具一致;JSON 必须是对象,不能传数组或裸字符串。"), (0, react.createElement)("h4", { className: "nwe-command-subhead" }, "常用组合"), (0, react.createElement)("ol", { className: "nwe-command-combos" }, ...COMMAND_COMBINATIONS.map((combination) => (0, react.createElement)("li", { key: combination.title }, (0, react.createElement)("strong", null, combination.title), (0, react.createElement)("code", { className: "nwe-command-steps" }, combination.steps.join(" → ")), (0, react.createElement)("p", null, combination.note)))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "边界:"), " /novel 不带参数等于 status;/novel projects --yes 必须和 --prune 一起用;/novel scan 不选书,--register 只登记;/novel resume 只读。短命令不会改变能力,只是 /novel 子命令的别名。"));
3043
+ };
2322
3044
  const tutorialDialog = (onClose, closeRef) => (0, react.createElement)("div", {
2323
3045
  className: "nwe-modal-backdrop",
2324
3046
  onMouseDown: (event) => {
@@ -2357,7 +3079,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2357
3079
  ref: closeRef,
2358
3080
  onClick: onClose,
2359
3081
  "aria-label": "关闭使用教程"
2360
- }, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)(CopyPrompt, {
3082
+ }, (0, react.createElement)("span", { "aria-hidden": "true" }, "×"))), (0, react.createElement)("div", { className: "nwe-modal-body" }, (0, react.createElement)("div", { className: "nwe-tutorial-intro" }, (0, react.createElement)("p", null, "网文工坊不是一个“帮你想剧情”的按钮,而是一台有状态的写作引擎:你提供内容,引擎负责阶段、字数、门控、评审、归档和文件。"), (0, react.createElement)("p", null, "每一步都先领取当前阶段的任务书,再提交;提交后看引擎返回的 blocking 与下一步。面板只读,真正的写操作都在对话里的工具调用中。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "开始前:记住三件事"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "只用引擎入口写"), (0, react.createElement)("span", null, "正文只走 novel_write_draft / novel_revise;状态只走工具。手改 chapters/、state/、outline/ 会破坏事务与校验。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "每一步先领 brief"), (0, react.createElement)("span", null, "任务书携带当前上下文和 digest;跳过它会撞 PG-01。任务书过期就重新领取,不要复用旧上下文。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "失败先读 code"), (0, react.createElement)("span", null, "参数错改参数,门控没过改正文,系统故障查目录/稍后重试。blocking 会一次列全,一次改完再交。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "用哪个会话:"), " 想获得完整的引擎保证,请新开一个「网文工坊」预设会话;普通会话里模型可能还挂着文件工具,能绕过引擎直接改稿。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一本书的完整流程"), (0, react.createElement)("p", null, "前四步是开书准备,后面是每章循环。第一次先按顺序走完一章,熟悉后再批量推进整本。"), (0, react.createElement)("ol", { className: "nwe-steps" }, ...TUTORIAL_STEPS.map((step, index) => tutorialStep(step, index)))), commandReferenceSection(), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "面板怎么看"), (0, react.createElement)("div", { className: "nwe-tutorial-cards" }, (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "顶部统计"), (0, react.createElement)("span", null, "已归档章、总字数、平均分、待处理门控。数字来自引擎,不是正文估算。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "九阶段轨与下一步"), (0, react.createElement)("span", null, "亮起的阶段是当前进度,“下一步”卡会直接给出该调用的工具名。")), (0, react.createElement)("div", { className: "nwe-tutorial-card" }, (0, react.createElement)("strong", null, "章节与门控"), (0, react.createElement)("span", null, "章节卡展开可看字数、评分与状态;门控区按 code/detail/hint 列出所有阻塞。"))), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "刷新:"), " 面板每 15 秒静默同步一次,也可以点右上角刷新。它是只读投影;不会替你改稿、归档或修状态。"), (0, react.createElement)("p", { className: "nwe-tutorial-note" }, (0, react.createElement)("strong", null, "项目状态:"), "选择器里 choose 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;不是项目表示目录还在但没有 novel.yaml 或项目状态;项目不可读表示权限或 IO 错误,先修权限或换一本。面板选择只影响查看,不改变任何会话的写作绑定。")), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "卡住时先看这里"), (0, react.createElement)("ul", { className: "nwe-faq" }, (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "为什么一直让我先领 brief?"), (0, react.createElement)("p", null, "这是正常门控。任务书决定当前阶段可用的字段与上下文 digest;先按返回值调用 novel_brief,再继续。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "门控失败,改一条还是全改?"), (0, react.createElement)("p", null, "一次看完所有 blocking,再一起改。逐条试会反复烧掉阶段轮次;错误里的 code 会告诉你属于参数、门控还是系统故障。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "字数和评分为什么对不上?"), (0, react.createElement)("p", null, "字数以 engine 返回为准,可用 novel_word_count 核对;review 的 total 必须等于 150 减全部扣分,不能另算一套。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "P4/P6 过不去怎么办?"), (0, react.createElement)("p", null, "P4 有 blocker 就领 P5 任务书修订,再回 P4 重评;P6 失败同样回修订。不要 waive blocker,也不要只跑半个 scope 就归档。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "想改已经归档的章节?"), (0, react.createElement)("p", null, "先 novel_reopen,带非空 reason。它会从归档备份恢复,重开后重新走 P3→P8;不要直接编辑归档卡或正文文件。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "状态文档坏了能手工修吗?"), (0, react.createElement)("p", null, "先用 novel_state 看 raw 和 next;需要修复时用 novel_amend,并在 reason 写清原因。手改 YAML 会丢失事务保护。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "有多个项目,面板看的是哪本?"), (0, react.createElement)("p", null, "面板用顶部项目选择器切换查看对象;选择只改 ?root= 和浏览器 localStorage,不调用 activate、不写会话绑定。写作项目请用 /novel use 或显式 project_root。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "选错书、想清掉绑定怎么办?"), (0, react.createElement)("p", null, "先用 /novel current 看当前会话绑定;要换书用 /novel use <路径|书名|.>,要清掉只影响当前会话的绑定用 /novel unbind。不要靠 active 猜,也不要手改 state/session-bindings.yaml。")), (0, react.createElement)("li", null, (0, react.createElement)("strong", null, "扫描到了,为什么还没切过去?"), (0, react.createElement)("p", null, "scan 只读发现;scan --register 只登记项目,不选择、不改 active、不绑会话。真正切换写作项目要用 /novel use,或第一次调用显式传 project_root。")))), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "可以直接复制给模型的起手话术"), (0, react.createElement)(CopyPrompt, {
2361
3083
  text: TUTORIAL_PROMPT,
2362
3084
  label: "复制起手话术"
2363
3085
  })), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "续写下一章时,直接复制这段给 Agent"), (0, react.createElement)("p", null, "这段会先确认状态,再处理缺章卡和目标章数已满,最后按 P1→P8 推进;归档前会要求你确认 state_delta。"), (0, react.createElement)(CopyPrompt, {
@@ -2595,11 +3317,32 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2595
3317
  }
2596
3318
  }, (0, react.createElement)("div", { className: "nwe-state-icon" }, stateIcon()), (0, react.createElement)("div", { className: "nwe-state-title" }, "书架还是空的"), (0, react.createElement)("div", { className: "nwe-state-text" }, "完成第一次归档后,章节会出现在这里。")) : (0, react.createElement)("ul", { className: "nwe-chapters" }, ...view.rows.slice(-30).map(chapterCard)));
2597
3319
  const v6Kv = (entries) => (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...entries.map(([key, value]) => (0, react.createElement)("li", { key }, (0, react.createElement)("b", null, key), (0, react.createElement)("span", null, value))));
2598
- const v6Card = (key, title, children, open = false) => (0, react.createElement)("details", {
2599
- className: "nwe-v6-card",
3320
+ const INFO_GLYPHS = {
3321
+ bp: "map",
3322
+ progress: "chart",
3323
+ volumes: "book",
3324
+ cast: "person",
3325
+ world: "map",
3326
+ foreshadow: "seed",
3327
+ quality: "chart",
3328
+ health: "shield"
3329
+ };
3330
+ const infoCard = (key, title, children, open = false) => (0, react.createElement)("details", {
3331
+ className: "nwe-info-card",
2600
3332
  key,
2601
3333
  ...open ? { open: true } : {}
2602
- }, (0, react.createElement)("summary", null, title), ...children);
3334
+ }, (0, react.createElement)("summary", { className: "nwe-info-card-head" }, (0, react.createElement)("span", {
3335
+ className: "nwe-info-card-icon",
3336
+ "aria-hidden": "true"
3337
+ }, sectionGlyph(INFO_GLYPHS[key] ?? "book", 16)), (0, react.createElement)("strong", { className: "nwe-info-card-title" }, title), (0, react.createElement)("span", {
3338
+ className: "nwe-info-card-caret",
3339
+ "aria-hidden": "true"
3340
+ }, "+")), (0, react.createElement)("div", {
3341
+ className: "nwe-info-card-body",
3342
+ role: "region",
3343
+ "aria-label": `${title} 内容`,
3344
+ tabIndex: 0
3345
+ }, ...children));
2603
3346
  const bookDataSection = (view) => {
2604
3347
  const data = view.bookData;
2605
3348
  if (data === null) return null;
@@ -2618,7 +3361,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2618
3361
  return (0, react.createElement)("section", {
2619
3362
  className: "nwe-section",
2620
3363
  "aria-label": "书籍数据"
2621
- }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "这本书"), (0, react.createElement)("span", { className: "nwe-section-note" }, "只读投影 · 不含正文")), (0, react.createElement)("div", { className: "nwe-v6-grid" }, v6Card("bp", "书脊与蓝图", [v6Kv([
3364
+ }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "这本书"), (0, react.createElement)("span", { className: "nwe-section-note" }, "只读投影 · 不含正文")), (0, react.createElement)("div", { className: "nwe-info-grid" }, infoCard("bp", "书脊与蓝图", [v6Kv([
2622
3365
  ["作者", data.identity.author || "未填写"],
2623
3366
  ["简介", data.identity.description || "未填写"],
2624
3367
  ["创建", localTime(data.identity.created_at)],
@@ -2627,7 +3370,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2627
3370
  ["基调 / POV", `${data.blueprint.tone || "—"} / ${data.blueprint.pov || "—"}`],
2628
3371
  ["读者 / 卖点", `${data.blueprint.audience || "—"} / ${data.blueprint.selling_points.join(" / ") || "—"}`],
2629
3372
  ["结局", data.blueprint.ending || "—"]
2630
- ]), (0, react.createElement)("p", { className: "nwe-v6-note" }, data.blueprint.logline || data.blueprint.premise || "还没有蓝图摘要")], true), v6Card("progress", "进度与当前章", [v6Kv([
3373
+ ]), (0, react.createElement)("p", { className: "nwe-v6-note" }, data.blueprint.logline || data.blueprint.premise || "还没有蓝图摘要")], true), infoCard("progress", "进度与当前章", [v6Kv([
2631
3374
  ["已归档", `${data.progress.archived} / ${data.progress.target ?? "不限"} 章`],
2632
3375
  ["字数", `归档 ${data.progress.words_archived} · 含草稿 ${data.progress.words_with_draft}`],
2633
3376
  ["剩余", data.progress.remaining_words === null ? "—" : `${data.progress.remaining_words} 字(${remainingBasisLabel(data.progress.remaining_words_basis)})`],
@@ -2635,7 +3378,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2635
3378
  ["轮次", `评 ${data.current.rounds.review} / 修 ${data.current.rounds.revise} / 检 ${data.current.rounds.check}`],
2636
3379
  ["正文 hash", data.current.manuscript_hash ?? "—"],
2637
3380
  ["当前字数", data.current.metrics === null ? "—" : String(data.current.metrics.total_word_count)]
2638
- ]), data.current.card === null ? null : (0, react.createElement)("div", { className: "nwe-v6-tags" }, ...data.current.card.cast.map((name) => (0, react.createElement)("span", { key: name }, name)))]), v6Card("volumes", "卷进度", [view.book.volumes.length === 0 ? (0, react.createElement)("p", { className: "nwe-v6-note" }, "还没有卷") : (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...view.book.volumes.map((volume) => (0, react.createElement)("li", { key: `${volume.title}-${volume.range}` }, (0, react.createElement)("b", null, volume.title), (0, react.createElement)("span", null, `${volume.range} · ${volume.planned} 计划 / ${volume.archived} 归档`))))]), v6Card("cast", "人物与关系", [v6Kv([["角色总数", String(data.cast.total)], ["关系", `${data.relationships.total} 条 · ${Object.entries(data.relationships.types).map(([type, count]) => `${type} ${count}`).join(" / ") || "—"}`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.cast.top.map((person) => (0, react.createElement)("li", { key: person.name }, (0, react.createElement)("b", null, person.name), (0, react.createElement)("span", null, `${person.role} · 出场 ${person.appearances} · 最近第 ${person.last_seen} 章`))))]), v6Card("world", "世界与设定", [
3381
+ ]), data.current.card === null ? null : (0, react.createElement)("div", { className: "nwe-v6-tags" }, ...data.current.card.cast.map((name) => (0, react.createElement)("span", { key: name }, name)))]), infoCard("volumes", "卷进度", [view.book.volumes.length === 0 ? (0, react.createElement)("p", { className: "nwe-v6-note" }, "还没有卷") : (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...view.book.volumes.map((volume) => (0, react.createElement)("li", { key: `${volume.title}-${volume.range}` }, (0, react.createElement)("b", null, volume.title), (0, react.createElement)("span", null, `${volume.range} · ${volume.planned} 计划 / ${volume.archived} 归档`))))]), infoCard("cast", "人物与关系", [v6Kv([["角色总数", String(data.cast.total)], ["关系", `${data.relationships.total} 条 · ${Object.entries(data.relationships.types).map(([type, count]) => `${type} ${count}`).join(" / ") || "—"}`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.cast.top.map((person) => (0, react.createElement)("li", { key: person.name }, (0, react.createElement)("b", null, person.name), (0, react.createElement)("span", null, `${person.role} · 出场 ${person.appearances} · 最近第 ${person.last_seen} 章`))))]), infoCard("world", "世界与设定", [
2639
3382
  v6Kv([
2640
3383
  ["地点 / 势力", `${data.worldbook.counts.locations} / ${data.worldbook.counts.factions}`],
2641
3384
  ["规则 / 术语", `${data.worldbook.counts.rules} / ${data.worldbook.counts.glossary}`],
@@ -2648,7 +3391,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2648
3391
  className: "nwe-v6-note",
2649
3392
  key: fact.alias
2650
3393
  }, `${fact.alias} = ${fact.value} ${fact.unit}`))
2651
- ]), v6Card("foreshadow", "伏笔与时间线", [v6Kv([["伏笔", `未解 ${data.foreshadows.open} · 休眠 ${data.foreshadows.dormant} · 逾期 ${data.foreshadows.overdue} · 已解 ${data.foreshadows.resolved}`], ["时间线", `${data.timeline.events} 个事件 · 第 ${data.timeline.first_chapter ?? "—"}–${data.timeline.last_chapter ?? "—"} 章`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.foreshadows.oldest.map((item) => (0, react.createElement)("li", { key: item.id }, (0, react.createElement)("b", null, item.id), (0, react.createElement)("span", null, `${item.title} · 已 ${item.age} 章`))))]), v6Card("quality", "质量与节奏", [
3394
+ ]), infoCard("foreshadow", "伏笔与时间线", [v6Kv([["伏笔", `未解 ${data.foreshadows.open} · 休眠 ${data.foreshadows.dormant} · 逾期 ${data.foreshadows.overdue} · 已解 ${data.foreshadows.resolved}`], ["时间线", `${data.timeline.events} 个事件 · 第 ${data.timeline.first_chapter ?? "—"}–${data.timeline.last_chapter ?? "—"} 章`]]), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.foreshadows.oldest.map((item) => (0, react.createElement)("li", { key: item.id }, (0, react.createElement)("b", null, item.id), (0, react.createElement)("span", null, `${item.title} · 已 ${item.age} 章`))))]), infoCard("quality", "质量与节奏", [
2652
3395
  v6Kv([
2653
3396
  ["全书均分", `${data.quality.average.toFixed(1)} · 中位 ${data.quality.median.toFixed(1)} · ${data.quality.min}–${data.quality.max}`],
2654
3397
  ["近十均分", data.quality.last_ten_average === null ? "—" : data.quality.last_ten_average.toFixed(1)],
@@ -2663,8 +3406,8 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2663
3406
  className: "nwe-v6-bars",
2664
3407
  "aria-label": "十五维质量"
2665
3408
  }, ...dims),
2666
- (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "逐章 effort"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.effort.chapters.map((chapter) => (0, react.createElement)("li", { key: chapter.num }, (0, react.createElement)("b", null, `第 ${chapter.num} 章`), (0, react.createElement)("span", null, `评 ${chapter.reviews} / 修 ${chapter.revisions} / 检 ${chapter.checks} / 回滚 ${chapter.rollbacks} · ${finalCheckLabel(chapter.final_check)}`)))))
2667
- ]), v6Card("health", "健康与恢复", [
3409
+ foldBlock("effort", "逐章 effort", (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...data.effort.chapters.map((chapter) => (0, react.createElement)("li", { key: chapter.num }, (0, react.createElement)("b", null, `第 ${chapter.num} 章`), (0, react.createElement)("span", null, `评 ${chapter.reviews} / 修 ${chapter.revisions} / 检 ${chapter.checks} / 回滚 ${chapter.rollbacks} · ${finalCheckLabel(chapter.final_check)}`)))), { meta: `${data.effort.chapters.length} 章` })
3410
+ ]), infoCard("health", "健康与恢复", [
2668
3411
  v6Kv([
2669
3412
  ["大纲", data.health.outline.present ? "可读" : data.health.outline.error === null ? "还没有大纲" : "损坏"],
2670
3413
  ["世界观", data.health.worldbook.present ? "可读" : data.health.worldbook.error === null ? "还没有设定" : "损坏"],
@@ -2681,7 +3424,18 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2681
3424
  return (0, react.createElement)("details", {
2682
3425
  className: "nwe-dossier",
2683
3426
  id: "nwe-dossier"
2684
- }, (0, react.createElement)("summary", null, (0, react.createElement)("span", null, "全书资料"), (0, react.createElement)("span", { className: "nwe-dossier-note" }, "蓝图 · 卷卡 · 世界 · 质量 · 健康")), body);
3427
+ }, (0, react.createElement)("summary", null, (0, react.createElement)("span", {
3428
+ className: "nwe-dossier-icon",
3429
+ "aria-hidden": "true"
3430
+ }, sectionGlyph("book", 16)), (0, react.createElement)("span", { className: "nwe-dossier-copy" }, (0, react.createElement)("strong", { className: "nwe-dossier-title" }, "全书资料"), (0, react.createElement)("span", { className: "nwe-dossier-note" }, "蓝图 · 卷卡 · 世界 · 质量 · 健康")), (0, react.createElement)("span", {
3431
+ className: "nwe-dossier-caret",
3432
+ "aria-hidden": "true"
3433
+ }, "+")), (0, react.createElement)("div", {
3434
+ className: "nwe-dossier-body",
3435
+ role: "region",
3436
+ "aria-label": "全书资料内容",
3437
+ tabIndex: 0
3438
+ }, body));
2685
3439
  };
2686
3440
  const PANEL_TABS = [
2687
3441
  {
@@ -2788,7 +3542,13 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2788
3542
  }, item.label)));
2789
3543
  const asRow = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
2790
3544
  const textOf = (value, fallback = "—") => typeof value === "string" && value !== "" ? value : typeof value === "number" && Number.isFinite(value) ? String(value) : fallback;
2791
- const roleLabel = (value) => value === "protagonist" ? "主角" : value === "supporting" ? "配角" : value === "extra" ? "龙套" : textOf(value);
3545
+ const ROLE_LABELS = {
3546
+ protagonist: "主角",
3547
+ supporting: "配角",
3548
+ antagonist: "反派",
3549
+ extra: "龙套"
3550
+ };
3551
+ const roleLabel = (value) => typeof value === "string" ? ROLE_LABELS[value] ?? textOf(value) : textOf(value);
2792
3552
  const STATUS_LABELS = {
2793
3553
  planted: "已埋",
2794
3554
  advanced: "已推进",
@@ -2943,7 +3703,12 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2943
3703
  };
2944
3704
  const sectionList = (title, items, kind = "spark") => {
2945
3705
  if (!Array.isArray(items) || items.length === 0) return (0, react.createElement)("div", { className: "nwe-empty" }, `${title}:暂无数据`);
2946
- return (0, react.createElement)("div", { className: "nwe-card-grid" }, ...items.map((item, index) => {
3706
+ return (0, react.createElement)("div", {
3707
+ className: "nwe-card-grid",
3708
+ role: "region",
3709
+ "aria-label": `${title}列表,可滚动`,
3710
+ tabIndex: 0
3711
+ }, ...items.map((item, index) => {
2947
3712
  const row = asRow(item);
2948
3713
  const explicitLabel = row === null ? void 0 : row.a !== void 0 && row.b !== void 0 ? `${textOf(row.a)} ↔ ${textOf(row.b)}` : row.title ?? row.name ?? row.id ?? row.alias ?? row.term ?? row.event ?? row.a;
2949
3714
  const label = row === null ? String(item) : textOf(row.chapter === void 0 ? explicitLabel : `第 ${textOf(row.chapter)} 章${row.time === void 0 ? "" : ` · ${textOf(row.time)}`}`, explicitLabel === void 0 ? title : textOf(explicitLabel));
@@ -2965,7 +3730,17 @@ brief P6 → check → brief P8 → archive 的顺序推进。
2965
3730
  }, sectionGlyph(kind)), (0, react.createElement)("span", { className: "nwe-data-card-heading" }, (0, react.createElement)("strong", null, label), note === "" ? null : (0, react.createElement)("span", { className: "nwe-data-card-note" }, note), chips.length === 0 ? null : (0, react.createElement)("span", { className: "nwe-data-card-chips" }, ...chips.map((chip) => (0, react.createElement)("span", { key: chip }, chip)))), (0, react.createElement)("span", {
2966
3731
  className: "nwe-data-card-caret",
2967
3732
  "aria-hidden": "true"
2968
- }, "+")), row === null ? (0, react.createElement)("div", { className: "nwe-data-card-body" }, String(item)) : (0, react.createElement)("div", { className: "nwe-data-card-body" }, sectionKv(row)));
3733
+ }, "+")), row === null ? (0, react.createElement)("div", {
3734
+ className: "nwe-data-card-body",
3735
+ role: "region",
3736
+ "aria-label": `${label} 内容`,
3737
+ tabIndex: 0
3738
+ }, String(item)) : (0, react.createElement)("div", {
3739
+ className: "nwe-data-card-body",
3740
+ role: "region",
3741
+ "aria-label": `${label} 内容`,
3742
+ tabIndex: 0
3743
+ }, sectionKv(row)));
2969
3744
  }));
2970
3745
  };
2971
3746
  const sectionBlock = (title, items, key) => {
@@ -3197,13 +3972,20 @@ brief P6 → check → brief P8 → archive 的顺序推进。
3197
3972
  const rawSummaryEvents = items?.summary_events ?? detailRow?.summary_events;
3198
3973
  const summaryEvents = Array.isArray(rawSummaryEvents) ? rawSummaryEvents.filter((event) => typeof event === "string") : [];
3199
3974
  return (0, react.createElement)("section", {
3200
- className: "nwe-chapter-detail",
3975
+ className: "nwe-chapter-detail nwe-chapter-detail-panel",
3201
3976
  "aria-label": `第 ${detail.chapter} 章详情`
3202
3977
  }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h3", { className: "nwe-section-title" }, `第 ${detail.chapter} 章详情`), (0, react.createElement)("button", {
3203
3978
  type: "button",
3204
3979
  className: "nwe-refresh",
3205
3980
  onClick: () => onClose(null)
3206
- }, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取章节详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `章节详情读取失败:${detail.section.error}`) : (0, react.createElement)(react.Fragment, null, manuscript === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "正文尚未写入。") : (0, react.createElement)("details", { open: true }, (0, react.createElement)("summary", null, `正文 · ${Array.from(manuscript).length} 字`), (0, react.createElement)("pre", { className: "nwe-detail-text" }, manuscript)), plan === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "归档 plan"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, plan)), worldPlan === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "世界计划"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, worldPlan)), summaryEvents.length === 0 ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "本章摘要"), (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...summaryEvents.map((event, index) => (0, react.createElement)("li", { key: `${index}-${event}` }, event))))));
3981
+ }, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取章节详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `章节详情读取失败:${detail.section.error}`) : (0, react.createElement)(react.Fragment, null, manuscript === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "正文尚未写入。") : foldBlock("manuscript", "正文", (0, react.createElement)("pre", { className: "nwe-detail-text" }, manuscript), {
3982
+ meta: `${Array.from(manuscript).length} 字`,
3983
+ open: true,
3984
+ className: "nwe-detail-fold"
3985
+ }), plan === null ? null : foldBlock("plan", "归档 plan", (0, react.createElement)("pre", { className: "nwe-detail-text" }, plan), { className: "nwe-detail-fold" }), worldPlan === null ? null : foldBlock("world", "世界计划", (0, react.createElement)("pre", { className: "nwe-detail-text" }, worldPlan), { className: "nwe-detail-fold" }), summaryEvents.length === 0 ? null : foldBlock("summary-events", "本章摘要", (0, react.createElement)("ul", { className: "nwe-v6-list" }, ...summaryEvents.map((event, index) => (0, react.createElement)("li", { key: `${index}-${event}` }, event))), {
3986
+ meta: `${summaryEvents.length} 条`,
3987
+ className: "nwe-detail-fold"
3988
+ })));
3207
3989
  };
3208
3990
  const qualityDetailPanel = (detail, onClose) => {
3209
3991
  if (detail === null) return null;
@@ -3212,13 +3994,16 @@ brief P6 → check → brief P8 → archive 的顺序推进。
3212
3994
  const review = typeof items?.review === "string" ? items.review : null;
3213
3995
  const check = typeof items?.check === "string" ? items.check : null;
3214
3996
  return (0, react.createElement)("section", {
3215
- className: "nwe-chapter-detail",
3997
+ className: "nwe-chapter-detail nwe-chapter-detail-panel",
3216
3998
  "aria-label": `第 ${detail.chapter} 章质量详情`
3217
3999
  }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h3", { className: "nwe-section-title" }, `第 ${detail.chapter} 章质量详情`), (0, react.createElement)("button", {
3218
4000
  type: "button",
3219
4001
  className: "nwe-refresh",
3220
4002
  onClick: () => onClose(null)
3221
- }, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取质量详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `质量详情读取失败:${detail.section.error}`) : review === null && check === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "还没有评审或检查文档。") : (0, react.createElement)(react.Fragment, null, review === null ? null : (0, react.createElement)("details", { open: true }, (0, react.createElement)("summary", null, "评审 review"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, review)), check === null ? null : (0, react.createElement)("details", null, (0, react.createElement)("summary", null, "检查 check"), (0, react.createElement)("pre", { className: "nwe-detail-text" }, check))));
4003
+ }, "收起")), detail.section.loading && payload === null ? (0, react.createElement)("div", { className: "nwe-section-note" }, "正在读取质量详情…") : detail.section.error !== null ? (0, react.createElement)("div", { className: "nwe-empty" }, `质量详情读取失败:${detail.section.error}`) : review === null && check === null ? (0, react.createElement)("div", { className: "nwe-empty" }, "还没有评审或检查文档。") : (0, react.createElement)(react.Fragment, null, review === null ? null : foldBlock("review", "评审 review", (0, react.createElement)("pre", { className: "nwe-detail-text" }, review), {
4004
+ open: true,
4005
+ className: "nwe-detail-fold"
4006
+ }), check === null ? null : foldBlock("check", "检查 check", (0, react.createElement)("pre", { className: "nwe-detail-text" }, check), { className: "nwe-detail-fold" })));
3222
4007
  };
3223
4008
  const chapterTable = (section, view, query, onQuery, detail = null, onDetail = () => void 0) => {
3224
4009
  if (section === null || section.loading && section.payload === null) return chaptersSection(view);
@@ -3244,7 +4029,12 @@ brief P6 → check → brief P8 → archive 的顺序推进。
3244
4029
  return (0, react.createElement)("section", {
3245
4030
  className: "nwe-chapters-section",
3246
4031
  "aria-label": "章节表"
3247
- }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, payload?.truncated === true && rows.length < total ? `显示 ${rows.length} / 共 ${total}` : payload?.truncated === true ? `共 ${total} 章 · 部分文本已截断` : `共 ${total} 章`)), rows.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "没有匹配的章节。") : (0, react.createElement)("div", { className: "nwe-table-wrap" }, (0, react.createElement)("table", { className: "nwe-chapter-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, ...[
4032
+ }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, payload?.truncated === true && rows.length < total ? `显示 ${rows.length} / 共 ${total}` : payload?.truncated === true ? `共 ${total} 章 · 部分文本已截断` : `共 ${total} 章`)), rows.length === 0 ? (0, react.createElement)("div", { className: "nwe-empty" }, "没有匹配的章节。") : (0, react.createElement)("div", {
4033
+ className: "nwe-table-wrap",
4034
+ role: "region",
4035
+ "aria-label": "章节表,可滚动",
4036
+ tabIndex: 0
4037
+ }, (0, react.createElement)("table", { className: "nwe-chapter-table" }, (0, react.createElement)("thead", null, (0, react.createElement)("tr", null, ...[
3248
4038
  "章",
3249
4039
  "标题",
3250
4040
  "状态",
@@ -3329,7 +4119,9 @@ brief P6 → check → brief P8 → archive 的顺序推进。
3329
4119
  "data-plugin": "dsh-novel-write-engine",
3330
4120
  className: "nwe-root",
3331
4121
  "aria-busy": loading,
3332
- "data-state": view.state
4122
+ "aria-label": "网文工坊页面",
4123
+ "data-state": view.state,
4124
+ tabIndex: 0
3333
4125
  }, (0, react.createElement)("style", { key: "nwe-style" }, CSS), (0, react.createElement)("header", { className: "nwe-hero" }, (0, react.createElement)("div", { className: "nwe-brand" }, (0, react.createElement)("span", {
3334
4126
  className: "nwe-brand-mark",
3335
4127
  "aria-hidden": "true"
@@ -3345,7 +4137,11 @@ brief P6 → check → brief P8 → archive 的顺序推进。
3345
4137
  }, refreshIcon(), loading ? "刷新中…" : "刷新"))), ready && view.damaged ? (0, react.createElement)("div", {
3346
4138
  className: "nwe-damaged",
3347
4139
  role: "status"
3348
- }, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready && view.nextTitle !== "" ? nextCard(view) : null, ready ? tabBar(tab, onTab) : null, ready && tab === "overview" ? (0, react.createElement)("div", {
4140
+ }, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready && view.nextTitle !== "" ? nextCard(view) : null, ready ? tabBar(tab, onTab) : null, (0, react.createElement)("div", {
4141
+ className: "nwe-workbench",
4142
+ role: "region",
4143
+ "aria-label": "网文工坊内容"
4144
+ }, ready && tab === "overview" ? (0, react.createElement)("div", {
3349
4145
  className: "nwe-stats",
3350
4146
  role: "group",
3351
4147
  "aria-label": "项目统计"
@@ -3367,7 +4163,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
3367
4163
  role: "tabpanel",
3368
4164
  id: `nwe-panel-${tab}`,
3369
4165
  "aria-labelledby": `nwe-tab-${tab}`
3370
- }, tabIntro(tab, view, section), tab === "health" ? (0, react.createElement)(react.Fragment, null, blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view)) : null, sectionPanel(tab, section, sectionSearch, onSectionSearch, view.chapter, qualityDetail, onQualityDetail)) : null, footer(view, onRefresh));
4166
+ }, tabIntro(tab, view, section), tab === "health" ? (0, react.createElement)(react.Fragment, null, blockingSection(view), advisoriesSection(view), pendingSection(view), legacySection(view)) : null, sectionPanel(tab, section, sectionSearch, onSectionSearch, view.chapter, qualityDetail, onQualityDetail)) : null, footer(view, onRefresh)));
3371
4167
  }
3372
4168
  const PANEL_ROOT_KEY = "nwe.panel-root.v1";
3373
4169
  const storage = () => globalThis.localStorage;