dsh-novel-write-engine 0.2.2 → 0.2.4

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
@@ -42,7 +42,7 @@ window.__ModuleLoader__.load({
42
42
  novel_word_count: "按引擎字数口径核对正文长度。"
43
43
  };
44
44
  // src/client/viewmodel.ts
45
- const PANEL_SCHEMA_VERSION = 4;
45
+ const PANEL_SCHEMA_VERSION = 5;
46
46
  const PHASE_LABELS = {
47
47
  P1: "世界观",
48
48
  P2: "章节计划",
@@ -152,7 +152,10 @@ window.__ModuleLoader__.load({
152
152
  quality: {
153
153
  averageScore: 0,
154
154
  scores: [],
155
- trend: 0
155
+ trend: null,
156
+ trendText: "—",
157
+ samples: 0,
158
+ attempts: 0
156
159
  },
157
160
  health: {
158
161
  ok: true,
@@ -160,6 +163,9 @@ window.__ModuleLoader__.load({
160
163
  stateErrors: 0
161
164
  },
162
165
  activity: [],
166
+ advisories: [],
167
+ acknowledgedAdvisories: 0,
168
+ legacy: [],
163
169
  nextTitle: "",
164
170
  nextDetail: "",
165
171
  nextTool: "",
@@ -174,24 +180,40 @@ window.__ModuleLoader__.load({
174
180
  subtitle: message,
175
181
  stageLabel: "连接异常"
176
182
  });
177
- const pathTail = (root) => {
178
- const parts = root.split(/[\\/]/).filter((part) => part !== "");
179
- return parts[parts.length - 1] ?? root;
183
+ const pathParts = (root) => root.split(/[\\/]/).filter((part) => part !== "");
184
+ /** Shortest suffix that is unique among projects sharing a display title. */
185
+ const pathHint = (root, sameTitleRoots) => {
186
+ const parts = pathParts(root);
187
+ if (parts.length === 0) return root;
188
+ const candidates = sameTitleRoots.map((other) => pathParts(other));
189
+ for (let depth = 1; depth <= parts.length; depth += 1) {
190
+ const suffix = parts.slice(-depth).join("/");
191
+ if (candidates.filter((candidate) => candidate.slice(-depth).join("/") === suffix).length === 1) return suffix;
192
+ }
193
+ return root;
180
194
  };
181
195
  const projectViews = (payload) => {
182
196
  const selected = payload.selection.root;
183
197
  const titleCounts = /* @__PURE__ */ new Map();
184
- for (const project of payload.projects ?? []) titleCounts.set(project.title, (titleCounts.get(project.title) ?? 0) + 1);
198
+ const titleRoots = /* @__PURE__ */ new Map();
199
+ for (const project of payload.projects ?? []) {
200
+ titleCounts.set(project.title, (titleCounts.get(project.title) ?? 0) + 1);
201
+ const roots = titleRoots.get(project.title) ?? [];
202
+ roots.push(project.root);
203
+ titleRoots.set(project.title, roots);
204
+ }
185
205
  return (payload.projects ?? []).map((project) => {
186
206
  const duplicate = (titleCounts.get(project.title) ?? 0) > 1;
187
- const state = project.state === "ok" ? "" : project.state === "damaged" ? "(待修复)" : project.state === "missing" ? "(目录不存在)" : "(不可读)";
207
+ const state = project.identity_changed === true ? "(路径已换书)" : project.state === "ok" ? "" : project.state === "damaged" ? "(待修复)" : project.state === "missing" ? "(目录不存在)" : project.state === "not-project" ? "(不是项目)" : "(不可读)";
188
208
  return {
189
209
  root: project.root,
190
210
  title: project.title,
191
- label: `《${project.title}》${duplicate ? ` — ${pathTail(project.root)}` : ""}${state}`,
211
+ label: `《${project.title}》${duplicate ? ` — ${pathHint(project.root, titleRoots.get(project.title) ?? [])}` : ""}${state}`,
192
212
  active: project.active,
193
213
  registered: project.registered,
194
214
  state: project.state,
215
+ ...project.identity_changed === true ? { identity_changed: true } : {},
216
+ group: project.identity_changed === true ? "damaged" : project.state === "ok" ? "writing" : project.state === "damaged" ? "damaged" : "missing",
195
217
  selected: selected === project.root
196
218
  };
197
219
  });
@@ -229,9 +251,9 @@ window.__ModuleLoader__.load({
229
251
  ...loading(),
230
252
  ...base,
231
253
  state: "missing",
232
- title: payload.missing.reason === "unreadable" ? "项目不可读" : "项目不在了",
254
+ title: payload.missing.reason === "busy" ? "正在写入" : payload.missing.reason === "unreadable" ? "项目不可读" : payload.missing.reason === "not-project" ? "不是项目目录" : "项目不在了",
233
255
  subtitle: `${payload.missing.title} · ${payload.missing.root}`,
234
- stageLabel: "不可用",
256
+ stageLabel: payload.missing.reason === "busy" ? "稍后刷新" : "不可用",
235
257
  missing: {
236
258
  root: payload.missing.root,
237
259
  title: payload.missing.title,
@@ -352,7 +374,10 @@ window.__ModuleLoader__.load({
352
374
  quality: {
353
375
  averageScore: summary.quality?.averageScore ?? average,
354
376
  scores: summary.quality?.scores ?? [],
355
- trend: summary.quality?.trend ?? 0
377
+ trend: summary.quality?.trend ?? null,
378
+ trendText: summary.quality?.trend === null || summary.quality?.trend === void 0 ? summary.quality?.trend_basis === "insufficient_samples" ? "样本不足" : summary.quality?.trend_basis === "insufficient_history" ? "历史不足" : "—" : `${summary.quality.trend >= 0 ? "+" : ""}${summary.quality.trend}`,
379
+ samples: summary.quality?.samples ?? summary.totalChapters,
380
+ attempts: summary.quality?.attempts ?? summary.quality?.samples ?? 0
356
381
  },
357
382
  health: {
358
383
  ok: summary.health?.ok ?? blocking.length === 0,
@@ -360,6 +385,13 @@ window.__ModuleLoader__.load({
360
385
  stateErrors: summary.health?.stateErrors ?? 0
361
386
  },
362
387
  activity: summary.activity ?? [],
388
+ advisories: (summary.advisories ?? []).map((item) => ({
389
+ code: item.code,
390
+ detail: item.detail,
391
+ hint: item.hint ?? ""
392
+ })),
393
+ acknowledgedAdvisories: summary.acknowledgedAdvisories ?? 0,
394
+ legacy: summary.legacy ?? [],
363
395
  nextTitle: finished ? "" : next === void 0 ? "状态已同步" : toolLabel(next.tool),
364
396
  nextDetail: finished ? "" : next === void 0 ? "没有待执行的下一步,刷新查看最新状态。" : NEXT_DETAILS[next.tool] ?? next.reason,
365
397
  nextTool: next?.tool ?? "",
@@ -405,9 +437,6 @@ window.__ModuleLoader__.load({
405
437
  --nwe-surface: var(--dsw-alias-bg-layer-1, #171a20);
406
438
  --nwe-raised: var(--dsw-alias-bg-layer-2, #1e222a);
407
439
  --nwe-border: var(--dsw-alias-border-l1, #ffffff14);
408
- --nwe-gold: #8f6a28;
409
- --nwe-jade: #3f7f69;
410
- --nwe-rose: #9e5454;
411
440
  --nwe-gold: color-mix(in srgb, #b7862f 82%, var(--nwe-ink));
412
441
  --nwe-jade: color-mix(in srgb, #4f9f82 68%, var(--nwe-ink));
413
442
  --nwe-rose: color-mix(in srgb, #c96a6a 68%, var(--nwe-ink));
@@ -723,7 +752,7 @@ window.__ModuleLoader__.load({
723
752
  .nwe-health { display: flex; align-items: center; gap: 8px; margin-top: 10px; color: var(--nwe-muted); font-size: 11px; }
724
753
  .nwe-health.is-bad { color: var(--nwe-rose); }
725
754
  .nwe-activity { display: flex; flex-direction: column; gap: 6px; margin: 10px 0 0; padding: 0; list-style: none; }
726
- .nwe-activity li { display: grid; grid-template-columns: 74px minmax(0, 1fr) auto; gap: 8px; color: var(--nwe-muted); font-size: 11px; }
755
+ .nwe-activity li { display: grid; grid-template-columns: 96px minmax(0, 1fr) auto; gap: 8px; color: var(--nwe-muted); font-size: 11px; }
727
756
  .nwe-activity code { color: var(--nwe-ink); font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 10px; }
728
757
  .nwe-next {
729
758
  display: flex;
@@ -841,6 +870,181 @@ window.__ModuleLoader__.load({
841
870
  .nwe-next-tool { display: none; }
842
871
  .nwe-footer { flex-direction: column; }
843
872
  }
873
+ /* v5 desk: one accent, compact chrome, content before decoration. */
874
+ .nwe-root {
875
+ max-width: 1180px;
876
+ padding: clamp(14px, 2.2vw, 28px);
877
+ background: transparent;
878
+ }
879
+ .nwe-root::before { display: none; }
880
+ .nwe-hero {
881
+ align-items: center;
882
+ padding: 14px 16px;
883
+ border-radius: 12px;
884
+ background: color-mix(in srgb, var(--nwe-surface) 94%, var(--nwe-raised));
885
+ box-shadow: none;
886
+ }
887
+ .nwe-hero-title { min-width: 0; }
888
+ .nwe-eyebrow { color: var(--nwe-muted); font-size: 9px; letter-spacing: .16em; }
889
+ .nwe-title {
890
+ margin: 4px 0 4px;
891
+ font-family: inherit;
892
+ font-size: clamp(21px, 2.4vw, 28px);
893
+ font-weight: 650;
894
+ letter-spacing: -.015em;
895
+ }
896
+ .nwe-meta { gap: 7px; font-size: 11px; }
897
+ .nwe-actions { gap: 7px; }
898
+ .nwe-project { flex-wrap: wrap; gap: 5px; }
899
+ .nwe-project-label { font-size: 10px; }
900
+ .nwe-project-select {
901
+ max-width: 310px;
902
+ min-height: 32px;
903
+ padding: 5px 26px 5px 9px;
904
+ border-radius: 8px;
905
+ background: var(--nwe-surface);
906
+ font-size: 11px;
907
+ }
908
+ .nwe-project-search {
909
+ width: 124px;
910
+ min-height: 32px;
911
+ padding: 5px 8px;
912
+ border: 1px solid var(--nwe-border);
913
+ border-radius: 8px;
914
+ background: var(--nwe-surface);
915
+ color: var(--nwe-ink);
916
+ font: inherit;
917
+ font-size: 11px;
918
+ }
919
+ .nwe-project-toggle {
920
+ display: inline-flex;
921
+ align-items: center;
922
+ gap: 4px;
923
+ color: var(--nwe-muted);
924
+ font-size: 10px;
925
+ cursor: pointer;
926
+ white-space: nowrap;
927
+ }
928
+ .nwe-refresh, .nwe-help { min-height: 32px; padding: 6px 10px; border-radius: 8px; }
929
+ .nwe-rootline { font-size: 9px; margin-top: 3px; }
930
+ .nwe-stats {
931
+ display: grid;
932
+ grid-template-columns: repeat(4, minmax(0, 1fr));
933
+ gap: 0;
934
+ margin-top: 10px;
935
+ padding: 9px 0;
936
+ border-top: 1px solid var(--nwe-border);
937
+ border-bottom: 1px solid var(--nwe-border);
938
+ }
939
+ .nwe-stat {
940
+ padding: 0 12px;
941
+ border: 0;
942
+ border-right: 1px solid var(--nwe-border);
943
+ border-radius: 0;
944
+ background: transparent;
945
+ }
946
+ .nwe-stat:last-child { border-right: 0; }
947
+ .nwe-stat-label { font-size: 10px; }
948
+ .nwe-stat-value { margin-top: 1px; font-size: 18px; font-weight: 600; }
949
+ .nwe-section { margin-top: 18px; }
950
+ .nwe-section-title { font-family: inherit; font-size: 14px; font-weight: 650; }
951
+ .nwe-section-note { font-size: 10px; }
952
+ .nwe-next {
953
+ margin-top: 12px;
954
+ padding: 14px 16px;
955
+ border: 1px solid color-mix(in srgb, var(--nwe-gold) 32%, var(--nwe-border));
956
+ border-left: 3px solid var(--nwe-gold);
957
+ border-radius: 10px;
958
+ background: color-mix(in srgb, var(--nwe-gold) 5%, var(--nwe-surface));
959
+ }
960
+ .nwe-next-icon { width: 24px; height: 24px; border-radius: 7px; }
961
+ .nwe-next-body { min-width: 0; }
962
+ .nwe-next-title { font-size: 14px; }
963
+ .nwe-next-detail { font-size: 11px; }
964
+ .nwe-next-tool { font-size: 9px; }
965
+ .nwe-desk {
966
+ display: grid;
967
+ grid-template-columns: minmax(0, 1.2fr) minmax(300px, .8fr);
968
+ gap: 18px;
969
+ align-items: start;
970
+ }
971
+ .nwe-desk-main, .nwe-desk-side { min-width: 0; }
972
+ .nwe-desk-side { padding-left: 18px; border-left: 1px solid var(--nwe-border); }
973
+ .nwe-desk-heading {
974
+ display: flex;
975
+ align-items: baseline;
976
+ justify-content: space-between;
977
+ gap: 8px;
978
+ margin-top: 16px;
979
+ }
980
+ .nwe-desk-kicker { color: var(--nwe-muted); font-size: 9px; font-weight: 650; letter-spacing: .14em; text-transform: uppercase; }
981
+ .nwe-desk-note { color: var(--nwe-gold); font-size: 10px; }
982
+ .nwe-desk-main .nwe-phases { margin-top: 8px; }
983
+ .nwe-desk-side .nwe-book-grid { grid-template-columns: 1fr; }
984
+ .nwe-desk-side .nwe-section { margin-top: 16px; }
985
+ .nwe-advisories, .nwe-legacy-list { display: flex; flex-direction: column; gap: 5px; margin: 8px 0 0; padding: 0; list-style: none; }
986
+ .nwe-advisory {
987
+ display: grid;
988
+ grid-template-columns: auto minmax(0, 1fr) auto;
989
+ gap: 8px;
990
+ align-items: baseline;
991
+ padding: 7px 9px;
992
+ border: 1px solid color-mix(in srgb, #c9a227 24%, var(--nwe-border));
993
+ border-radius: 7px;
994
+ background: color-mix(in srgb, #c9a227 5%, transparent);
995
+ font-size: 11px;
996
+ }
997
+ .nwe-advisory-code { color: #b08a1e; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 9px; white-space: nowrap; }
998
+ .nwe-advisory-detail { color: var(--nwe-ink); line-height: 1.5; }
999
+ .nwe-advisory-hint { color: var(--nwe-muted); font-size: 9px; white-space: nowrap; }
1000
+ .nwe-advisory-empty {
1001
+ margin-top: 8px;
1002
+ padding: 8px 10px;
1003
+ border: 1px solid color-mix(in srgb, #c9a227 18%, var(--nwe-border));
1004
+ border-radius: 7px;
1005
+ color: var(--nwe-muted);
1006
+ font-size: 11px;
1007
+ }
1008
+ .nwe-legacy {
1009
+ display: flex;
1010
+ gap: 8px;
1011
+ align-items: baseline;
1012
+ padding: 6px 8px;
1013
+ border: 1px dashed var(--nwe-border);
1014
+ border-radius: 7px;
1015
+ color: var(--nwe-muted);
1016
+ font-size: 10px;
1017
+ line-height: 1.5;
1018
+ }
1019
+ .nwe-legacy code { color: var(--nwe-muted); font-size: 9px; white-space: nowrap; }
1020
+ .nwe-chapters-section { margin-top: 20px; }
1021
+ .nwe-desk-main .nwe-chapters {
1022
+ max-height: 460px;
1023
+ overflow-y: auto;
1024
+ padding-right: 4px;
1025
+ scrollbar-gutter: stable;
1026
+ }
1027
+ .nwe-chapter { border-radius: 9px; }
1028
+ .nwe-chapter summary { padding: 10px 13px; }
1029
+ .nwe-chapter-detail { padding-left: 68px; }
1030
+ @media (max-width: 900px) {
1031
+ .nwe-desk { grid-template-columns: 1fr; gap: 0; }
1032
+ .nwe-desk-side { padding-left: 0; border-left: 0; }
1033
+ .nwe-desk-side .nwe-section { margin-top: 18px; }
1034
+ }
1035
+ @media (max-width: 760px) {
1036
+ .nwe-stats { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 0; padding: 8px 0; }
1037
+ .nwe-stat:nth-child(2) { border-right: 0; }
1038
+ .nwe-stat { padding: 0 8px; }
1039
+ .nwe-project { order: 3; width: 100%; }
1040
+ .nwe-project-select { flex: 1; max-width: none; }
1041
+ .nwe-project-search { flex: 1 1 100%; width: auto; }
1042
+ .nwe-project-toggle { flex: 1 1 100%; }
1043
+ .nwe-desk-main .nwe-chapters { max-height: 360px; }
1044
+ .nwe-chapter-detail { padding-left: 18px; }
1045
+ .nwe-advisory { grid-template-columns: 1fr; gap: 2px; }
1046
+ .nwe-advisory-hint { white-space: normal; }
1047
+ }
844
1048
  @media (prefers-reduced-motion: reduce) {
845
1049
  .nwe-skeleton-line { animation: none; }
846
1050
  .nwe-refresh, .nwe-chapter { transition: none; }
@@ -967,7 +1171,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
967
1171
  如果当前会话还没绑定或不确定是哪本书:先用 /novel current 看会话绑定,再调用 novel_status 看 selection;需要切换时用 /novel use <路径|书名|.>,不要靠 active 猜。
968
1172
  第一次调用带 project_root 时,它会成为当前会话的持久绑定;后续无 root 调用会继续这本书。
969
1173
 
970
- 如果下一章还没有章卡:先用 novel_state {kind:"outline"} 取出全部章卡,再在末尾加上下一章的卡,用 novel_outline 整体提交;不要只提交新卡,也不要把已有 planned 卡丢掉。
1174
+ 如果下一章还没有章卡:用 novel_outline 只提交下一章的卡(按 num upsert),省略的已有卡会保持不动;不要重交全量,也不要用空数组清空。要删卡必须显式传 remove。
971
1175
  如果 targets.total_chapters 已经写满:先用 novel_amend 把 total_chapters 调到至少包含下一章,不能低于已归档章数。
972
1176
 
973
1177
  之后严格按:novel_brief P1 → novel_plan_world → brief P2 → novel_plan_chapter → brief P3 → novel_write_draft → brief P4 → novel_review →(有 blocker)brief P5 → novel_revise → 回 P4 → brief P6 → novel_check → brief P8 → novel_archive 的顺序推进。
@@ -1036,7 +1240,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1036
1240
  }, `${label} ${value}`))), (0, react.createElement)("div", {
1037
1241
  className: "nwe-book-head",
1038
1242
  style: { marginTop: 12 }
1039
- }, (0, react.createElement)("span", { className: "nwe-book-title" }, "质量趋势"), (0, react.createElement)("span", { className: "nwe-book-note" }, bars.length === 0 ? "—" : `${view.quality.averageScore.toFixed(1)} 均分`)), (0, react.createElement)("div", {
1243
+ }, (0, react.createElement)("span", { className: "nwe-book-title" }, "质量趋势"), (0, react.createElement)("span", { className: "nwe-book-note" }, bars.length === 0 ? "—" : `${view.quality.averageScore.toFixed(1)} 近十均分 · 趋势 ${view.quality.trendText} · ${view.quality.samples} 章 / ${view.quality.attempts} 评`)), (0, react.createElement)("div", {
1040
1244
  className: "nwe-bars",
1041
1245
  role: "img",
1042
1246
  "aria-label": "最近十章评分"
@@ -1051,6 +1255,27 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1051
1255
  if (view.activity.length === 0) return null;
1052
1256
  return (0, react.createElement)("section", { className: "nwe-section" }, (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)("ul", { className: "nwe-activity" }, ...view.activity.slice(-5).map((row, index) => (0, react.createElement)("li", { key: `${row.at}-${row.op}-${index}` }, (0, react.createElement)("span", null, row.at.slice(0, 16).replace("T", " ")), (0, react.createElement)("code", null, row.op), (0, react.createElement)("span", null, row.chapter === 0 ? row.status : `第 ${row.chapter} 章 · ${row.status}`)))));
1053
1257
  };
1258
+ const advisoriesSection = (view) => {
1259
+ if (view.advisories.length === 0 && view.acknowledgedAdvisories === 0) return null;
1260
+ const note = view.acknowledgedAdvisories === 0 ? "不阻塞归档 · 可 acknowledge" : `已确认 ${view.acknowledgedAdvisories} 条 · 不阻塞归档`;
1261
+ return (0, react.createElement)("section", {
1262
+ className: "nwe-section nwe-advisory-section",
1263
+ "aria-label": "质量提示"
1264
+ }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "提示"), (0, react.createElement)("span", { className: "nwe-section-note" }, note)), view.advisories.length === 0 ? (0, react.createElement)("div", { className: "nwe-advisory-empty" }, "本轮提示已全部确认。") : (0, react.createElement)("ul", { className: "nwe-advisories" }, ...view.advisories.map((item) => (0, react.createElement)("li", {
1265
+ className: "nwe-advisory",
1266
+ key: `${item.code}-${item.detail}`
1267
+ }, (0, react.createElement)("code", { className: "nwe-advisory-code" }, item.code), (0, react.createElement)("span", { className: "nwe-advisory-detail" }, item.detail), item.hint === "" ? null : (0, react.createElement)("span", { className: "nwe-advisory-hint" }, item.hint)))));
1268
+ };
1269
+ const legacySection = (view) => {
1270
+ if (view.legacy.length === 0) return null;
1271
+ return (0, react.createElement)("section", {
1272
+ className: "nwe-section nwe-legacy-section",
1273
+ "aria-label": "旧数据提示"
1274
+ }, (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)("ul", { className: "nwe-legacy-list" }, ...view.legacy.map((item) => (0, react.createElement)("li", {
1275
+ className: "nwe-legacy",
1276
+ key: `${item.code}-${item.detail}`
1277
+ }, (0, react.createElement)("code", null, item.code), (0, react.createElement)("span", null, item.detail)))));
1278
+ };
1054
1279
  const footer = () => (0, react.createElement)("footer", { className: "nwe-footer" }, (0, react.createElement)("span", null, "只读面板 · 数据来自引擎 · 每 15 秒自动同步"), (0, react.createElement)("code", null, "novel-workshop"));
1055
1280
  const loadingState = () => (0, react.createElement)("div", {
1056
1281
  className: "nwe-section",
@@ -1063,9 +1288,51 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1063
1288
  className: "nwe-section-note",
1064
1289
  style: { marginTop: 12 }
1065
1290
  }, "正在读取项目状态…"));
1066
- const projectSelector = (view, onSelect) => {
1291
+ const ProjectSelector = ({ view, onSelect }) => {
1292
+ const [query, setQuery] = (0, react.useState)("");
1293
+ const [showMissing, setShowMissing] = (0, react.useState)(false);
1294
+ const searchableCount = view.projects.filter((project) => project.group !== "missing" || showMissing || project.root === view.selectedRoot).length;
1295
+ (0, react.useEffect)(() => {
1296
+ if (searchableCount <= 6 && query !== "") setQuery("");
1297
+ }, [searchableCount, query]);
1067
1298
  if (view.projects.length === 0) return null;
1068
- return (0, react.createElement)("div", { className: "nwe-project" }, (0, react.createElement)("label", {
1299
+ const normalized = query.trim().toLowerCase();
1300
+ const selected = view.projects.find((project) => project.root === view.selectedRoot);
1301
+ const visible = view.projects.filter((project) => {
1302
+ const groupVisible = project.group !== "missing" || showMissing || project.root === view.selectedRoot;
1303
+ const queryVisible = normalized === "" || project.title.toLowerCase().includes(normalized) || project.root.toLowerCase().includes(normalized) || project.label.toLowerCase().includes(normalized);
1304
+ return groupVisible && queryVisible;
1305
+ });
1306
+ const groups = [
1307
+ {
1308
+ key: "writing",
1309
+ label: `写作中 (${view.projects.filter((p) => p.group === "writing").length})`,
1310
+ items: []
1311
+ },
1312
+ {
1313
+ key: "damaged",
1314
+ label: `待修复 (${view.projects.filter((p) => p.group === "damaged").length})`,
1315
+ items: []
1316
+ },
1317
+ {
1318
+ key: "missing",
1319
+ label: `失效 (${view.projects.filter((p) => p.group === "missing").length})`,
1320
+ items: []
1321
+ }
1322
+ ];
1323
+ for (const project of visible) groups.find((group) => group.key === project.group).items.push(project);
1324
+ groups[0].label = `写作中 (${groups[0].items.length})`;
1325
+ groups[1].label = `待修复 (${groups[1].items.length})`;
1326
+ groups[2].label = `失效 (${groups[2].items.length})`;
1327
+ const missingCount = view.projects.filter((project) => project.group === "missing").length;
1328
+ return (0, react.createElement)("div", { className: "nwe-project" }, searchableCount > 6 ? (0, react.createElement)("input", {
1329
+ className: "nwe-project-search",
1330
+ type: "search",
1331
+ value: query,
1332
+ placeholder: "搜索项目",
1333
+ "aria-label": "搜索项目",
1334
+ onChange: (event) => setQuery(event.target.value)
1335
+ }) : null, (0, react.createElement)("label", {
1069
1336
  className: "nwe-project-label",
1070
1337
  htmlFor: "nwe-project-select"
1071
1338
  }, "项目"), (0, react.createElement)("select", {
@@ -1076,24 +1343,35 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1076
1343
  onChange: (event) => {
1077
1344
  onSelect(event.target.value === "" ? null : event.target.value);
1078
1345
  }
1079
- }, view.selectedRoot === null ? (0, react.createElement)("option", { value: "" }, "选择项目…") : null, ...view.projects.map((project) => (0, react.createElement)("option", {
1346
+ }, view.selectedRoot === null ? (0, react.createElement)("option", { value: "" }, "选择项目…") : null, ...groups.filter((group) => group.items.length > 0).map((group) => (0, react.createElement)("optgroup", {
1347
+ key: group.key,
1348
+ label: group.label
1349
+ }, ...group.items.map((project) => (0, react.createElement)("option", {
1080
1350
  key: project.root,
1081
1351
  value: project.root
1082
- }, project.label))));
1352
+ }, project.label)))), selected !== void 0 && !visible.some((project) => project.root === selected.root) ? (0, react.createElement)("option", { value: selected.root }, selected.label) : null), missingCount > 0 ? (0, react.createElement)("label", { className: "nwe-project-toggle" }, (0, react.createElement)("input", {
1353
+ type: "checkbox",
1354
+ checked: showMissing,
1355
+ onChange: (event) => setShowMissing(event.target.checked)
1356
+ }), `显示失效 ${missingCount}`) : null);
1357
+ };
1358
+ const choosePanel = (view, onSelect) => {
1359
+ const preferred = view.projects.filter((project) => project.group !== "missing");
1360
+ const shown = preferred.length > 0 ? preferred : view.projects;
1361
+ return (0, react.createElement)("div", {
1362
+ className: "nwe-state",
1363
+ role: "status"
1364
+ }, (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" }, view.subtitle), (0, react.createElement)("div", { className: "nwe-project-choices" }, ...shown.map((project) => (0, react.createElement)("button", {
1365
+ key: project.root,
1366
+ type: "button",
1367
+ className: "nwe-project-choice",
1368
+ onClick: () => onSelect(project.root)
1369
+ }, project.label, (0, react.createElement)("span", null, project.root)))));
1083
1370
  };
1084
- const choosePanel = (view, onSelect) => (0, react.createElement)("div", {
1085
- className: "nwe-state",
1086
- role: "status"
1087
- }, (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" }, view.subtitle), (0, react.createElement)("div", { className: "nwe-project-choices" }, ...view.projects.map((project) => (0, react.createElement)("button", {
1088
- key: project.root,
1089
- type: "button",
1090
- className: "nwe-project-choice",
1091
- onClick: () => onSelect(project.root)
1092
- }, project.label, (0, react.createElement)("span", null, project.root)))));
1093
1371
  const missingPanel = (view) => (0, react.createElement)("div", {
1094
1372
  className: "nwe-state",
1095
1373
  role: "alert"
1096
- }, (0, react.createElement)("div", { className: "nwe-state-icon" }, stateIcon()), (0, react.createElement)("div", { className: "nwe-state-title" }, view.missing?.reason === "unreadable" ? "项目不可读" : "项目不在了"), (0, react.createElement)("div", { className: "nwe-state-text" }, view.missing?.hint ?? view.subtitle), view.missing === void 0 ? null : (0, react.createElement)("div", { className: "nwe-state-text" }, (0, react.createElement)("code", { className: "nwe-command" }, `${view.missing.code} · ${view.missing.reason}`), (0, react.createElement)("div", { className: "nwe-rootline" }, view.missing.root)));
1374
+ }, (0, react.createElement)("div", { className: "nwe-state-icon" }, stateIcon()), (0, react.createElement)("div", { className: "nwe-state-title" }, view.missing?.reason === "busy" ? "正在写入" : view.missing?.reason === "unreadable" ? "项目不可读" : view.missing?.reason === "not-project" ? "不是项目目录" : "项目不在了"), (0, react.createElement)("div", { className: "nwe-state-text" }, view.missing?.hint ?? view.subtitle), view.missing === void 0 ? null : (0, react.createElement)("div", { className: "nwe-state-text" }, (0, react.createElement)("code", { className: "nwe-command" }, `${view.missing.code} · ${view.missing.reason}`), (0, react.createElement)("div", { className: "nwe-rootline" }, view.missing.root)));
1097
1375
  const CopyResumeButton = ({ text }) => {
1098
1376
  const [status, setStatus] = (0, react.useState)("idle");
1099
1377
  (0, react.useEffect)(() => {
@@ -1179,7 +1457,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1179
1457
  ref: closeRef,
1180
1458
  onClick: onClose,
1181
1459
  "aria-label": "关闭使用教程"
1182
- }, (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 表示发现多本书、还没选;项目不在了表示目录缺失,绑定仍会保留;项目不可读表示权限或 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)("pre", { className: "nwe-prompt" }, TUTORIAL_PROMPT)), (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)("pre", { className: "nwe-prompt" }, TUTORIAL_NEXT_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, "教程基于 0.2.2 · 具体字段以当前任务书和工具返回为准 · 面板只读")));
1460
+ }, (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)("pre", { className: "nwe-prompt" }, TUTORIAL_PROMPT)), (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)("pre", { className: "nwe-prompt" }, TUTORIAL_NEXT_PROMPT)), (0, react.createElement)("section", { className: "nwe-tutorial-block" }, (0, react.createElement)("h3", null, "一章完成检查清单"), (0, react.createElement)("ul", { className: "nwe-checklist" }, (0, react.createElement)("li", null, "本章 P1/P2 计划已提交,P3 正文已通过格式与字数门控。"), (0, react.createElement)("li", null, "P4 评审 total 对账无误,没有未修 blocker。"), (0, react.createElement)("li", null, "P6 五个语义项都有不同句子的正文引用,完整 scope 通过。"), (0, react.createElement)("li", null, "P8 任务书当前,novel_archive 带完整 state_delta 成功。"), (0, react.createElement)("li", null, "面板出现已归档章节,门控区没有新的阻塞。")))), (0, react.createElement)("div", { className: "nwe-modal-foot" }, "教程基于 0.2.4 · 具体字段以当前任务书和工具返回为准 · 面板只读")));
1183
1461
  const NovelHelp = () => {
1184
1462
  const [open, setOpen] = (0, react.useState)(false);
1185
1463
  const triggerRef = (0, react.useRef)(null);
@@ -1223,6 +1501,30 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1223
1501
  title: "插件使用教程"
1224
1502
  }, helpIcon(), (0, react.createElement)("span", { className: "nwe-help-label" }, "教程")), open ? tutorialDialog(close, closeRef) : null);
1225
1503
  };
1504
+ const nextCard = (view) => (0, react.createElement)("section", {
1505
+ className: "nwe-next",
1506
+ "aria-label": "下一步"
1507
+ }, (0, react.createElement)("div", { className: "nwe-next-icon" }, arrowIcon()), (0, react.createElement)("div", { className: "nwe-next-body" }, (0, react.createElement)("div", { className: "nwe-next-kicker" }, "CURRENT CHAPTER · 下一步"), (0, react.createElement)("div", { className: "nwe-next-title" }, view.nextTitle), (0, react.createElement)("div", { className: "nwe-next-detail" }, view.nextDetail), (0, react.createElement)(CopyResumeButton, { text: view.resumeText })), view.nextTool === "" ? null : (0, react.createElement)("code", { className: "nwe-next-tool" }, view.nextTool));
1508
+ const blockingSection = (view) => (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "门控"), (0, react.createElement)("span", { className: "nwe-section-note" }, view.blockingTitle)), view.blocking.length === 0 ? (0, react.createElement)("div", {
1509
+ className: "nwe-pass",
1510
+ role: "status"
1511
+ }, (0, react.createElement)("span", { "aria-hidden": "true" }, "✓"), (0, react.createElement)("span", null, "当前阶段没有阻塞项,可以继续下一步。")) : (0, react.createElement)("ul", {
1512
+ className: "nwe-blocking",
1513
+ "aria-label": "阻塞项"
1514
+ }, ...view.blocking.map((item) => (0, react.createElement)("li", {
1515
+ className: "nwe-block",
1516
+ key: `${item.code}-${item.scope}-${item.detail}`
1517
+ }, (0, react.createElement)("span", {
1518
+ className: "nwe-block-icon",
1519
+ "aria-hidden": "true"
1520
+ }, "!"), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-block-code" }, item.code), (0, react.createElement)("div", { className: "nwe-block-detail" }, item.detail), item.hint === "" ? null : (0, react.createElement)("div", { className: "nwe-block-hint" }, item.hint))))));
1521
+ const chaptersSection = (view) => (0, react.createElement)("section", { className: "nwe-section nwe-chapters-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.stats.chapters} 条记录${view.stats.chapters > view.rows.filter((row) => row.tone === "ok").length ? ` · 显示最近 ${view.rows.length} 条` : ""}`)), view.rows.length === 0 ? (0, react.createElement)("div", {
1522
+ className: "nwe-empty",
1523
+ style: {
1524
+ marginTop: 12,
1525
+ minHeight: 170
1526
+ }
1527
+ }, (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)));
1226
1528
  /**
1227
1529
  * Pure view: given a view model, return the element tree. Keeping the hooks in
1228
1530
  * `NovelPanel` makes every visual state renderable without a browser.
@@ -1235,7 +1537,10 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1235
1537
  className: "nwe-root",
1236
1538
  "aria-busy": loading,
1237
1539
  "data-state": view.state
1238
- }, (0, react.createElement)("style", { key: "nwe-style" }, CSS), (0, react.createElement)("header", { className: "nwe-hero" }, (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-eyebrow" }, "NOVEL STUDIO · 网文工坊"), (0, react.createElement)("h1", { className: "nwe-title" }, view.title), (0, react.createElement)("div", { className: "nwe-meta" }, (0, react.createElement)("span", { className: `nwe-stage${view.finished ? " is-done" : ""}` }, view.stageLabel), ready ? (0, react.createElement)("span", null, view.subtitle) : null, ready ? (0, react.createElement)("span", { "aria-hidden": "true" }, "·") : null, ready ? (0, react.createElement)("span", null, view.revisionLabel) : null, view.state === "choose" || view.state === "missing" || view.state === "loading" ? (0, react.createElement)("span", null, view.subtitle) : null), ready && view.root !== "" ? (0, react.createElement)("div", { className: "nwe-rootline" }, view.root) : null), (0, react.createElement)("div", { className: "nwe-actions" }, selectable ? projectSelector(view, onSelectProject) : null, (0, react.createElement)(NovelHelp), (0, react.createElement)("button", {
1540
+ }, (0, react.createElement)("style", { key: "nwe-style" }, CSS), (0, react.createElement)("header", { className: "nwe-hero" }, (0, react.createElement)("div", { className: "nwe-hero-title" }, (0, react.createElement)("div", { className: "nwe-eyebrow" }, "NOVEL STUDIO · 网文工坊"), (0, react.createElement)("h1", { className: "nwe-title" }, view.title), (0, react.createElement)("div", { className: "nwe-meta" }, (0, react.createElement)("span", { className: `nwe-stage${view.finished ? " is-done" : ""}` }, view.stageLabel), ready ? (0, react.createElement)("span", null, view.subtitle) : null, ready ? (0, react.createElement)("span", { "aria-hidden": "true" }, "·") : null, ready ? (0, react.createElement)("span", null, view.revisionLabel) : null, view.state === "choose" || view.state === "missing" || view.state === "loading" ? (0, react.createElement)("span", null, view.subtitle) : null), ready && view.root !== "" ? (0, react.createElement)("div", { className: "nwe-rootline" }, view.root) : null), (0, react.createElement)("div", { className: "nwe-actions" }, selectable ? (0, react.createElement)(ProjectSelector, {
1541
+ view,
1542
+ onSelect: onSelectProject
1543
+ }) : null, (0, react.createElement)(NovelHelp), (0, react.createElement)("button", {
1239
1544
  type: "button",
1240
1545
  className: "nwe-refresh",
1241
1546
  onClick: onRefresh,
@@ -1244,32 +1549,14 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1244
1549
  }, refreshIcon(), loading ? "刷新中…" : "刷新"))), ready && view.damaged ? (0, react.createElement)("div", {
1245
1550
  className: "nwe-damaged",
1246
1551
  role: "status"
1247
- }, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready ? (0, react.createElement)("div", {
1552
+ }, "项目定义不可读;先用 novel_state 查看,再用 novel_amend 按 next 修复。面板不会把它伪装成正常项目。") : null, ready && view.nextTitle !== "" ? nextCard(view) : null, ready ? (0, react.createElement)("div", {
1248
1553
  className: "nwe-stats",
1249
1554
  role: "group",
1250
1555
  "aria-label": "项目统计"
1251
- }, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), "字"), stat("平均评分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready ? bookSection(view) : null, ready ? phases(view) : null, ready && view.nextTitle !== "" ? (0, react.createElement)("section", {
1252
- className: "nwe-next",
1253
- "aria-label": "下一步"
1254
- }, (0, react.createElement)("div", { className: "nwe-next-icon" }, arrowIcon()), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-next-kicker" }, "NEXT MOVE"), (0, react.createElement)("div", { className: "nwe-next-title" }, view.nextTitle), (0, react.createElement)("div", { className: "nwe-next-detail" }, view.nextDetail), (0, react.createElement)(CopyResumeButton, { text: view.resumeText })), view.nextTool === "" ? null : (0, react.createElement)("code", { className: "nwe-next-tool" }, view.nextTool)) : null, ready ? (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "章节"), (0, react.createElement)("span", { className: "nwe-section-note" }, `${view.stats.chapters} 条记录${view.stats.chapters > view.rows.filter((row) => row.tone === "ok").length ? ` · 显示最近 ${view.rows.length} 条` : ""}`)), view.rows.length === 0 ? (0, react.createElement)("div", {
1255
- className: "nwe-empty",
1256
- style: {
1257
- marginTop: 12,
1258
- minHeight: 170
1259
- }
1260
- }, (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))) : null, ready ? (0, react.createElement)("section", { className: "nwe-section" }, (0, react.createElement)("div", { className: "nwe-section-head" }, (0, react.createElement)("h2", { className: "nwe-section-title" }, "门控"), (0, react.createElement)("span", { className: "nwe-section-note" }, view.blockingTitle)), view.blocking.length === 0 ? (0, react.createElement)("div", {
1261
- className: "nwe-pass",
1262
- role: "status"
1263
- }, (0, react.createElement)("span", { "aria-hidden": "true" }, "✓"), (0, react.createElement)("span", null, "当前阶段没有阻塞项,可以继续下一步。")) : (0, react.createElement)("ul", {
1264
- className: "nwe-blocking",
1265
- "aria-label": "阻塞项"
1266
- }, ...view.blocking.map((item) => (0, react.createElement)("li", {
1267
- className: "nwe-block",
1268
- key: `${item.code}-${item.scope}-${item.detail}`
1269
- }, (0, react.createElement)("span", {
1270
- className: "nwe-block-icon",
1271
- "aria-hidden": "true"
1272
- }, "!"), (0, react.createElement)("div", null, (0, react.createElement)("div", { className: "nwe-block-code" }, item.code), (0, react.createElement)("div", { className: "nwe-block-detail" }, item.detail), item.hint === "" ? null : (0, react.createElement)("div", { className: "nwe-block-hint" }, item.hint)))))) : null, ready ? activitySection(view) : null, footer());
1556
+ }, stat("已归档", String(view.stats.chapters), "章"), stat("总字数", view.stats.words.toLocaleString("zh-CN"), "字"), stat("全书均分", view.stats.chapters === 0 ? "—" : String(view.stats.averageScore), "/150"), stat("待处理", String(view.stats.blocking), "项")) : null, view.state === "loading" ? loadingState() : null, view.state === "choose" ? choosePanel(view, onSelectProject) : null, view.state === "missing" ? missingPanel(view) : null, view.state === "error" || view.state === "empty" || view.state === "outdated" ? statePanel(view.state, view, loading, onRefresh) : null, ready ? (0, react.createElement)("div", { className: "nwe-desk" }, (0, react.createElement)("div", { className: "nwe-desk-main" }, (0, react.createElement)("div", { className: "nwe-desk-heading" }, (0, react.createElement)("span", { className: "nwe-desk-kicker" }, "当前一章"), (0, react.createElement)("span", { className: "nwe-desk-note" }, view.stageLabel)), phases(view), blockingSection(view), advisoriesSection(view), legacySection(view), chaptersSection(view)), (0, react.createElement)("aside", {
1557
+ className: "nwe-desk-side",
1558
+ "aria-label": "全书与活动"
1559
+ }, bookSection(view), activitySection(view))) : null, footer());
1273
1560
  }
1274
1561
  const PANEL_ROOT_KEY = "nwe.panel-root.v1";
1275
1562
  const storage = () => globalThis.localStorage;
@@ -1333,7 +1620,7 @@ brief P6 → check → brief P8 → archive 的顺序推进。
1333
1620
  if (payload !== null) setSelectedRoot(payload.selection.root);
1334
1621
  setView((previous) => {
1335
1622
  const next = buildView(payload, result.error);
1336
- if (silent && result.error !== null && previous.state === "ready") return previous;
1623
+ if (silent && result.error !== null && previous.projects.length > 0) return previous;
1337
1624
  return next;
1338
1625
  });
1339
1626
  } finally {