dsh-maoying 0.1.16 → 0.1.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +10 -10
- package/README.md +9 -9
- package/bin/maoying.mjs +2 -2
- package/docs/creator-panel.md +7 -2
- package/docs/design.md +6 -6
- package/docs/spec.md +5 -2
- package/index.mjs +17 -8
- package/lib/base.css +24 -10
- package/lib/client.js +132 -11
- package/lib/novel-parse.mjs +33 -1
- package/lib/panel-core.mjs +11 -1
- package/lib/self-update.mjs +408 -123
- package/package.json +1 -1
- package/presets/maoying/agent.cordis.v2.yml +13 -13
- package/presets/maoying/agent.cordis.yml +13 -13
- package/skills/maoying-review-checklist/SKILL.md +8 -3
- package/templates/novel/README.md +5 -1
- package/templates/novel/style.md +35 -0
package/lib/client.js
CHANGED
|
@@ -53,14 +53,17 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
var CSS_KEY = "maoying-creator/base";
|
|
54
54
|
var WS_STORAGE_KEY = "maoying.workspace";
|
|
55
55
|
var API_TIMEOUT_MS = 60000;
|
|
56
|
-
// 一键升级单独放宽:服务端 pnpm add 超时上限 180s
|
|
57
|
-
//
|
|
58
|
-
|
|
56
|
+
// 一键升级单独放宽:服务端 profile pnpm add 超时上限 180s + 全局 npm i -g
|
|
57
|
+
// 独立 120s(顺带更新全局 maoying CLI),统一 60s 会先掐断,故 self-update
|
|
58
|
+
// 请求走 340s(300s + 40s 余量),其余请求仍用 API_TIMEOUT_MS。
|
|
59
|
+
var SELF_UPDATE_TIMEOUT_MS = 340000;
|
|
59
60
|
|
|
60
61
|
var STATUS_FLOW = ["pending", "writing", "review", "accepted"];
|
|
61
62
|
var FS_ORDER = ["planted", "progressing", "resolved", "abandoned"];
|
|
62
63
|
var FS_FLOW = { planted: "progressing", progressing: "resolved" };
|
|
63
64
|
var PHASES = ["idea", "setting", "outline", "writing", "review", "consistency", "export"];
|
|
65
|
+
// 共创模式三态(key → mode.* / modeShort.* 词典后缀):概览书稿卡展示用。
|
|
66
|
+
var MODES = [["ai-draft", "aiDraft"], ["co-write", "coWrite"], ["author-draft", "authorDraft"]];
|
|
64
67
|
// clue.* 词典键是小驼峰,数据里的 kind 按契约(templates/novel/README.md、
|
|
65
68
|
// docs/spec.md)是 clue|red-herring|deduction|question——此前键表误按
|
|
66
69
|
// snake_case + inference 写,红鲱鱼/推理两组永远查不中、只能裸显英文原值。
|
|
@@ -283,7 +286,10 @@ window.__ModuleLoader__.load({
|
|
|
283
286
|
return toast.subscribe(function () { setItems(toast.snapshot()); });
|
|
284
287
|
}, []);
|
|
285
288
|
if (!items.length) return null;
|
|
286
|
-
|
|
289
|
+
// portal 到 body:与弹层同坐标系。中栏 transform/overflow 会把 fixed 劫持
|
|
290
|
+
// 成相对中栏定位,锚区变量(视口坐标)在此会被二次偏移;挂 body 后
|
|
291
|
+
// fixed 恒等于视口,--myt-anchor-* 直接可用(base.css 按锚区居中)。
|
|
292
|
+
return h(MaskPortal, { className: "myt-toastHost", "aria-live": "polite" },
|
|
287
293
|
items.map(function (it) {
|
|
288
294
|
return h("div", {
|
|
289
295
|
key: it.id,
|
|
@@ -571,6 +577,66 @@ window.__ModuleLoader__.load({
|
|
|
571
577
|
lsSet(FONT_SCALE_KEY, fontScale);
|
|
572
578
|
}, [fontScale]);
|
|
573
579
|
|
|
580
|
+
// 弹层锚区:弹层 portal 到 body 后 position:fixed 以全窗为居中基准,
|
|
581
|
+
// 宿主右侧边栏一开,中栏变窄左移,弹窗仍按全窗居中,右半被侧边栏盖住。
|
|
582
|
+
// 这里持续测量面板容器(data-dsh-maoying-view)的视口矩形写进 CSS 变量,
|
|
583
|
+
// 弹层/Toast 改按锚区居中(base.css);测量不到时移除变量回落全窗,行
|
|
584
|
+
// 为与旧版一致。宿主侧边栏开合会改中栏宽度,ResizeObserver + resize 双
|
|
585
|
+
// 通道跟随,rAF 合帧防抖。
|
|
586
|
+
// 两组变量:--myt-anchor-* 视觉 px(Toast 未 zoom,直接用);--myt-mask-*
|
|
587
|
+
// 已除以面板 zoom 的 CSS px(mask 带 zoom,fixed 内 px 会被放大,须预
|
|
588
|
+
// 除——JS 算好,避开 calc(px / var()) 的老 Chromium 兼容雷)。zoom 每次
|
|
589
|
+
// 现读 computed style,字号换档自动跟随,无需额外依赖。
|
|
590
|
+
useEffect(function () {
|
|
591
|
+
if (typeof document === "undefined" || typeof window === "undefined") return undefined;
|
|
592
|
+
var docEl = document.documentElement;
|
|
593
|
+
if (!docEl || !docEl.style) return undefined;
|
|
594
|
+
var ALL_VARS = [
|
|
595
|
+
"--myt-anchor-left", "--myt-anchor-width", "--myt-anchor-bottom",
|
|
596
|
+
"--myt-mask-top", "--myt-mask-right", "--myt-mask-bottom", "--myt-mask-left",
|
|
597
|
+
];
|
|
598
|
+
var clear = function () {
|
|
599
|
+
ALL_VARS.forEach(function (v) { docEl.style.removeProperty(v); });
|
|
600
|
+
};
|
|
601
|
+
var px = function (n) { return (Math.round(n * 100) / 100) + "px"; };
|
|
602
|
+
var apply = function () {
|
|
603
|
+
raf = 0;
|
|
604
|
+
var el = document.querySelector("[data-dsh-maoying-view]");
|
|
605
|
+
if (!el || !el.isConnected) { clear(); return; }
|
|
606
|
+
var r = el.getBoundingClientRect();
|
|
607
|
+
if (!r || r.width < 2 || r.height < 2) { clear(); return; } // display:none 等异常态不锚定
|
|
608
|
+
docEl.style.setProperty("--myt-anchor-left", px(r.left));
|
|
609
|
+
docEl.style.setProperty("--myt-anchor-width", px(r.width));
|
|
610
|
+
docEl.style.setProperty("--myt-anchor-bottom", px(Math.max(0, window.innerHeight - r.bottom)));
|
|
611
|
+
var zoom = 1;
|
|
612
|
+
try {
|
|
613
|
+
var cs = window.getComputedStyle(el);
|
|
614
|
+
var pz = cs && cs.zoom ? parseFloat(cs.zoom) : NaN;
|
|
615
|
+
if (pz > 0) zoom = pz;
|
|
616
|
+
} catch (err) { /* 老宿主无 computed zoom:按 1 处理 */ }
|
|
617
|
+
docEl.style.setProperty("--myt-mask-top", px(r.top / zoom));
|
|
618
|
+
docEl.style.setProperty("--myt-mask-right", px(Math.max(0, window.innerWidth - r.right) / zoom));
|
|
619
|
+
docEl.style.setProperty("--myt-mask-bottom", px(Math.max(0, window.innerHeight - r.bottom) / zoom));
|
|
620
|
+
docEl.style.setProperty("--myt-mask-left", px(r.left / zoom));
|
|
621
|
+
};
|
|
622
|
+
var raf = 0;
|
|
623
|
+
var schedule = function () {
|
|
624
|
+
if (raf) return;
|
|
625
|
+
raf = window.requestAnimationFrame(apply);
|
|
626
|
+
};
|
|
627
|
+
apply();
|
|
628
|
+
window.addEventListener("resize", schedule);
|
|
629
|
+
var ro = typeof ResizeObserver === "function" ? new ResizeObserver(schedule) : null;
|
|
630
|
+
var el = document.querySelector("[data-dsh-maoying-view]");
|
|
631
|
+
if (ro && el) ro.observe(el);
|
|
632
|
+
return function () {
|
|
633
|
+
if (raf) window.cancelAnimationFrame(raf);
|
|
634
|
+
window.removeEventListener("resize", schedule);
|
|
635
|
+
if (ro) ro.disconnect();
|
|
636
|
+
clear();
|
|
637
|
+
};
|
|
638
|
+
}, []);
|
|
639
|
+
|
|
574
640
|
// 选中校正:记忆的工作区必须仍在列表且 hasNovel,否则落回首个 hasNovel。
|
|
575
641
|
var effectiveWs = (function () {
|
|
576
642
|
if (!workspaces) return "";
|
|
@@ -633,24 +699,48 @@ window.__ModuleLoader__.load({
|
|
|
633
699
|
}, []);
|
|
634
700
|
// 一键升级状态机:""(可点)→ "installing"(安装中,按钮禁用)→
|
|
635
701
|
// "installed"(已装完待重启,按钮永久禁用、新版徽章收起)。可预期失败
|
|
636
|
-
// (dev-link / pnpm-missing / pnpm 出错)回落 ""
|
|
702
|
+
// (dev-link / pnpm-missing / pnpm 出错)回落 "" 允许重试;服务端短路
|
|
703
|
+
// up-to-date(本地已不落后线上)也回落 "",按钮恢复可点、不进 installed 态。
|
|
704
|
+
// 全局 maoying CLI 顺带更新结果(服务端 globalCli 字段)拼成 toast 第二句
|
|
705
|
+
// (\n 换行,配合 base.css myt-toastText 的 pre-line 显示):
|
|
706
|
+
// - ok 且真更新了(from → to):「全局 maoying CLI a → b」;
|
|
707
|
+
// - skipped 'missing'(用户没装全局 CLI)/ 'same'(已同版):不提,零噪音;
|
|
708
|
+
// - ok:false:如实报「全局 CLI 更新失败:{error}」,不改动主文案的成功语义
|
|
709
|
+
// (profile 已装好,重启照常生效)。
|
|
710
|
+
function globalCliNote(d) {
|
|
711
|
+
var g = d && d.globalCli;
|
|
712
|
+
if (!g) return "";
|
|
713
|
+
if (g.ok) {
|
|
714
|
+
if (g.skipped) return "";
|
|
715
|
+
return "\n" + tf("toast.selfUpdate.globalOk", { from: g.from || "?", to: g.to || "?" });
|
|
716
|
+
}
|
|
717
|
+
return "\n" + tf("toast.selfUpdate.globalFail", { error: g.error || tt("common.unknownErr") });
|
|
718
|
+
}
|
|
637
719
|
var _up = useState(""), upState = _up[0], setUpState = _up[1];
|
|
638
720
|
function runSelfUpdate() {
|
|
639
721
|
if (upState !== "") return;
|
|
640
722
|
setUpState("installing");
|
|
641
723
|
// 不走 apiPost:成功提示要带新版本号、失败按 kind 分流文案,且超时
|
|
642
|
-
// 单独放宽(服务端 pnpm 最长 180s
|
|
724
|
+
// 单独放宽(服务端 profile pnpm 最长 180s + 全局 npm 独立 120s,统一
|
|
725
|
+
// 60s 会先掐断,见常量处注释)。
|
|
643
726
|
maoyingApi("self-update", {}, SELF_UPDATE_TIMEOUT_MS).then(function (d) {
|
|
644
|
-
if (d && d.ok) {
|
|
727
|
+
if (d && d.ok && d.kind === "up-to-date") {
|
|
728
|
+
// 短路:profile 没装任何东西,但全局 CLI 仍可能要跟进(globalCliNote
|
|
729
|
+
// 拼第二句);按钮恢复可点;版本徽章留给 update-check 自行收敛。
|
|
730
|
+
setUpState("");
|
|
731
|
+
toast.ok(tf("toast.selfUpdate.upToDate", { ver: (d && d.from) || "?" }) + globalCliNote(d));
|
|
732
|
+
} else if (d && d.ok) {
|
|
645
733
|
setUpState("installed");
|
|
646
734
|
// 硬约束照实说:装完只是磁盘换新,运行中的进程仍是旧代码。
|
|
647
|
-
toast.ok(tf("toast.selfUpdate.ok", { ver: (d && d.to) || "?" }));
|
|
735
|
+
toast.ok(tf("toast.selfUpdate.ok", { ver: (d && d.to) || "?" }) + globalCliNote(d));
|
|
648
736
|
} else if (d && d.kind === "dev-link") {
|
|
649
737
|
setUpState("");
|
|
650
738
|
toast.warn(tt("toast.selfUpdate.devLink"));
|
|
651
739
|
} else if (d && d.kind === "pnpm-missing") {
|
|
740
|
+
// 服务端 message 已带可操作出路(corepack enable / 手动命令),直显不截断;
|
|
741
|
+
// 拿不到 message 才回落本地通用文案。
|
|
652
742
|
setUpState("");
|
|
653
|
-
toast.
|
|
743
|
+
toast.err(tf("toast.prefix", { msg: (d && d.error) || tt("toast.selfUpdate.pnpmMissing") }));
|
|
654
744
|
} else {
|
|
655
745
|
setUpState("");
|
|
656
746
|
toast.err(tf("toast.prefix", { msg: (d && d.error) || tt("common.unknownErr") }));
|
|
@@ -817,6 +907,9 @@ window.__ModuleLoader__.load({
|
|
|
817
907
|
var _b = useState(false), phaseBusy = _b[0], setPhaseBusy = _b[1];
|
|
818
908
|
var phaseIdx = PHASES.indexOf(project.phase);
|
|
819
909
|
var nextPhase = phaseIdx !== -1 && phaseIdx < PHASES.length - 1 ? PHASES[phaseIdx + 1] : null;
|
|
910
|
+
// 共创模式徽章:快照 mode 缺失(老项目无该键)或越枚举时不显示。
|
|
911
|
+
var modeEntry = null;
|
|
912
|
+
for (var mi = 0; mi < MODES.length; mi++) { if (MODES[mi][0] === project.mode) { modeEntry = MODES[mi]; break; } }
|
|
820
913
|
// 静态体检黄条:仅 found 时拉 check 路由(未初始化没有可查的东西)。
|
|
821
914
|
// 工作区切换与每次 reload 都换新 snap → effect 重跑即重查;请求序号防
|
|
822
915
|
// 过期响应(快速切工作区时在途旧响应不得落进新工作区的黄条)。
|
|
@@ -839,6 +932,7 @@ window.__ModuleLoader__.load({
|
|
|
839
932
|
apiPost("phase", { path: props.wsPath, phase: nextPhase }).then(reload).catch(function () {}).finally(function () { setPhaseBusy(false); });
|
|
840
933
|
}
|
|
841
934
|
var recent = (snap.chapters || []).slice().sort(function (a, b) { return (b.num || 0) - (a.num || 0); }).slice(0, 5);
|
|
935
|
+
var volumes = snap.volumes || [];
|
|
842
936
|
return h("div", { className: "myt-stack" },
|
|
843
937
|
checkProblems && checkProblems.length ? CheckStrip(checkProblems) : null,
|
|
844
938
|
editing
|
|
@@ -856,6 +950,10 @@ window.__ModuleLoader__.load({
|
|
|
856
950
|
h("h2", { className: "myt-heroTitle", key: "title" }, orDash(project.title)),
|
|
857
951
|
h("div", { className: "myt-heroMeta", key: "phase" },
|
|
858
952
|
phasePill(project.phase),
|
|
953
|
+
modeEntry ? [
|
|
954
|
+
h("span", { className: "myt-muted", key: "modeLabel" }, tt("novel.fMode")),
|
|
955
|
+
h("span", { className: "myt-badge", key: "modeBadge", title: tt("mode." + modeEntry[1]) }, tt("modeShort." + modeEntry[1])),
|
|
956
|
+
] : null,
|
|
859
957
|
nextPhase ? h("button", { type: "button", className: "myt-btn", disabled: phaseBusy, onClick: advancePhase, title: tf("phase.advanceTitle", { next: tt("phase." + nextPhase) }) }, phaseBusy ? tt("phase.advancing") : tt("phase.advance")) : h("span", { className: "myt-muted" }, project.phase ? tt("phase.final") : null)),
|
|
860
958
|
project.description ? h("p", { className: "myt-heroDesc", key: "desc" }, project.description) : null,
|
|
861
959
|
]),
|
|
@@ -877,6 +975,16 @@ window.__ModuleLoader__.load({
|
|
|
877
975
|
h("div", { className: "myt-heroMeta" }, STATUS_FLOW.map(function (s) {
|
|
878
976
|
return h("span", { key: s, className: "myt-muted" }, tt("status." + s) + " " + (byStatus[s] || 0));
|
|
879
977
|
}))),
|
|
978
|
+
volumes.length ? h("div", { className: "myt-card" },
|
|
979
|
+
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("overview.volumes")),
|
|
980
|
+
h("span", { className: "myt-boardCount" }, volumes.length)),
|
|
981
|
+
h("div", { className: "myt-stack" }, volumes.map(function (v, i) {
|
|
982
|
+
return h("div", { key: v.id || i, className: "myt-worldGroup" },
|
|
983
|
+
h("div", { className: "myt-listHead" },
|
|
984
|
+
h("span", { className: "myt-worldName" }, (v.id ? v.id + " · " : "") + orDash(v.title)),
|
|
985
|
+
v.chapters ? h("span", { className: "myt-tag" }, v.chapters) : null),
|
|
986
|
+
v.summary ? h("div", { className: "myt-worldDesc" }, v.summary) : null);
|
|
987
|
+
}))) : null,
|
|
880
988
|
h("div", { className: "myt-card" },
|
|
881
989
|
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("overview.recent"))),
|
|
882
990
|
recent.length
|
|
@@ -1010,6 +1118,8 @@ window.__ModuleLoader__.load({
|
|
|
1010
1118
|
"header.upgradeTitle": "用 pnpm 安装 npm 最新版(最长约 3 分钟),装完需重启 DeepSeek 生效",
|
|
1011
1119
|
"header.upgradedTitle": "已安装新版,重启 DeepSeek 后生效",
|
|
1012
1120
|
"toast.selfUpdate.ok": "已安装 {ver},重启 DeepSeek 后生效",
|
|
1121
|
+
"toast.selfUpdate.upToDate": "已是最新版({ver})",
|
|
1122
|
+
"toast.selfUpdate.globalOk": "全局 maoying CLI {from} → {to}", "toast.selfUpdate.globalFail": "全局 CLI 更新失败:{error}",
|
|
1013
1123
|
"toast.selfUpdate.devLink": "开发目录安装,请 git pull 更新", "toast.selfUpdate.pnpmMissing": "未找到 pnpm,请手动安装",
|
|
1014
1124
|
"header.workspace": "工作区", "header.wsAria": "选择工作区", "header.noNovel": "(无 novel)", "header.untitled": "未命名",
|
|
1015
1125
|
"header.settings": "系统设置", "header.refresh": "重新拉取", "header.closeAria": "关闭创作台", "header.closeTitle": "关闭",
|
|
@@ -1060,6 +1170,7 @@ window.__ModuleLoader__.load({
|
|
|
1060
1170
|
"char.modalAria": "角色详情", "char.closeAria": "关闭角色卡", "char.markers": "标记", "char.file": "文件",
|
|
1061
1171
|
"overview.author": "作者:{name}", "overview.totalWords": "总字数(CJK)", "overview.totalChapters": "章节总数",
|
|
1062
1172
|
"overview.characters": "角色数", "overview.openForeshadows": "未回收伏笔", "overview.statusDist": "章节状态分布", "overview.recent": "最近章节",
|
|
1173
|
+
"overview.volumes": "分卷规划",
|
|
1063
1174
|
"overview.edit": "编辑", "overview.editTip": "编辑书稿元信息",
|
|
1064
1175
|
"chapters.empty": "chapters/ 目录还没有章节文件", "chapters.writeQueue": "按单写作", "chapters.rowTip": "点击打开该章",
|
|
1065
1176
|
"chapters.outlineWords": "章纲 {n} 字", "chapters.all": "全部", "chapters.thVolume": "卷 · 章", "chapters.thTitle": "标题",
|
|
@@ -1080,6 +1191,7 @@ window.__ModuleLoader__.load({
|
|
|
1080
1191
|
"novel.lengthPh": "如:约100万字 / 300章", "novel.fLanguage": "正文语言", "novel.fPhase": "工作流阶段", "novel.fMode": "共创模式",
|
|
1081
1192
|
"novel.fDesc": "一句话简介", "novel.descPh": "写封面文案时可直接引用", "novel.titleRequired": "书名不能为空",
|
|
1082
1193
|
"mode.aiDraft": "AI 代笔(AI 写正文)", "mode.coWrite": "逐章共创(按章指派谁写)", "mode.authorDraft": "作者亲笔(AI 只备料审稿)",
|
|
1194
|
+
"modeShort.aiDraft": "AI 代笔", "modeShort.coWrite": "逐章共创", "modeShort.authorDraft": "作者亲笔",
|
|
1083
1195
|
"novel.saving": "保存中…", "novel.save": "保存设置", "novel.cancel": "取消",
|
|
1084
1196
|
"settings.title": "系统设置", "settings.closeAria": "关闭设置", "settings.language": "语言",
|
|
1085
1197
|
"settings.languageHint": "界面文字随语言选项切换,立即生效并记忆。", "settings.fontScale": "字号",
|
|
@@ -1095,6 +1207,8 @@ window.__ModuleLoader__.load({
|
|
|
1095
1207
|
"header.upgradeTitle": "Install the latest npm release with pnpm (up to ~3 min); takes effect after restarting DeepSeek",
|
|
1096
1208
|
"header.upgradedTitle": "New version installed — takes effect after restarting DeepSeek",
|
|
1097
1209
|
"toast.selfUpdate.ok": "Installed {ver} — takes effect after restarting DeepSeek",
|
|
1210
|
+
"toast.selfUpdate.upToDate": "Already up to date ({ver})",
|
|
1211
|
+
"toast.selfUpdate.globalOk": "Global maoying CLI {from} → {to}", "toast.selfUpdate.globalFail": "Global CLI update failed: {error}",
|
|
1098
1212
|
"toast.selfUpdate.devLink": "Installed from a dev directory — update with git pull", "toast.selfUpdate.pnpmMissing": "pnpm not found — install it manually",
|
|
1099
1213
|
"header.workspace": "Workspace", "header.wsAria": "Select workspace", "header.noNovel": "(no novel)", "header.untitled": "Untitled",
|
|
1100
1214
|
"header.settings": "Settings", "header.refresh": "Refresh", "header.closeAria": "Close Maoying Studio", "header.closeTitle": "Close",
|
|
@@ -1145,6 +1259,7 @@ window.__ModuleLoader__.load({
|
|
|
1145
1259
|
"char.modalAria": "Character details", "char.closeAria": "Close character card", "char.markers": "Tags", "char.file": "File",
|
|
1146
1260
|
"overview.author": "Author: {name}", "overview.totalWords": "Total characters (CJK)", "overview.totalChapters": "Chapters",
|
|
1147
1261
|
"overview.characters": "Characters", "overview.openForeshadows": "Unresolved foreshadowing", "overview.statusDist": "Chapter status breakdown", "overview.recent": "Recent chapters",
|
|
1262
|
+
"overview.volumes": "Volume plan",
|
|
1148
1263
|
"overview.edit": "Edit", "overview.editTip": "Edit manuscript metadata",
|
|
1149
1264
|
"chapters.empty": "No chapter files in chapters/ yet", "chapters.writeQueue": "Writing Queue", "chapters.rowTip": "Click to open this chapter",
|
|
1150
1265
|
"chapters.outlineWords": "Outline {n} chars", "chapters.all": "All", "chapters.thVolume": "Vol · Ch", "chapters.thTitle": "Title",
|
|
@@ -1165,6 +1280,7 @@ window.__ModuleLoader__.load({
|
|
|
1165
1280
|
"novel.lengthPh": "e.g. ~1M characters / 300 chapters", "novel.fLanguage": "Text language", "novel.fPhase": "Workflow phase", "novel.fMode": "Co-writing mode",
|
|
1166
1281
|
"novel.fDesc": "One-line synopsis", "novel.descPh": "Can be quoted directly in cover copy", "novel.titleRequired": "Title is required",
|
|
1167
1282
|
"mode.aiDraft": "AI ghostwriting (AI drafts the prose)", "mode.coWrite": "Chapter-by-chapter co-writing (assign a writer per chapter)", "mode.authorDraft": "Author writes (AI only preps and reviews)",
|
|
1283
|
+
"modeShort.aiDraft": "AI draft", "modeShort.coWrite": "Co-write", "modeShort.authorDraft": "Author writes",
|
|
1168
1284
|
"novel.saving": "Saving…", "novel.save": "Save", "novel.cancel": "Cancel",
|
|
1169
1285
|
"settings.title": "Settings", "settings.closeAria": "Close settings", "settings.language": "Language",
|
|
1170
1286
|
"settings.languageHint": "UI text follows the language option; it applies immediately and is remembered.", "settings.fontScale": "Font size",
|
|
@@ -1774,8 +1890,13 @@ window.__ModuleLoader__.load({
|
|
|
1774
1890
|
research.length ? h("div", { className: "myt-card" },
|
|
1775
1891
|
h("div", { className: "myt-cardHead" }, h("h3", { className: "myt-cardTitle" }, tt("world.research")),
|
|
1776
1892
|
h("span", { className: "myt-boardCount" }, research.length)),
|
|
1777
|
-
h("div", { className: "myt-
|
|
1778
|
-
return h("
|
|
1893
|
+
h("div", { className: "myt-stack" }, research.map(function (r, i) {
|
|
1894
|
+
return h("div", { key: r.file || i, className: "myt-worldGroup" },
|
|
1895
|
+
h("div", { className: "myt-listHead" },
|
|
1896
|
+
h("span", { className: "myt-worldName", title: r.file || "" }, orDash(r.topic) || r.file)),
|
|
1897
|
+
r.description ? h(MdView, { text: r.description, className: "myt-worldDesc" }) : null,
|
|
1898
|
+
(r.sources || []).length ? h("div", { className: "myt-refList" },
|
|
1899
|
+
r.sources.map(function (s, j) { return h("span", { key: j, className: "myt-refItem", title: s }, tf("world.ref", { n: j + 1, r: s })); })) : null);
|
|
1779
1900
|
}))) : null);
|
|
1780
1901
|
}
|
|
1781
1902
|
|
package/lib/novel-parse.mjs
CHANGED
|
@@ -709,6 +709,9 @@ const FORESHADOW_STATUSES = ['planted', 'progressing', 'resolved', 'abandoned']
|
|
|
709
709
|
/** project.yml 的共创模式枚举(五期 §9.1)。 */
|
|
710
710
|
const PROJECT_MODES = ['ai-draft', 'co-write', 'author-draft']
|
|
711
711
|
|
|
712
|
+
/** 七阶段工作流枚举(与 project.yml 的 phase 键一致,顺序即推进序)。 */
|
|
713
|
+
export const PROJECT_PHASES = ['idea', 'setting', 'outline', 'writing', 'review', 'consistency', 'export']
|
|
714
|
+
|
|
712
715
|
/**
|
|
713
716
|
* 章节 frontmatter 的 volume 规范形态:v + 两位卷号。面板从章纲建章
|
|
714
717
|
* (panel-core 的 createChapterFromOutline)与本文件 check / collectChapters
|
|
@@ -947,7 +950,10 @@ export function checkProject(novelDir) {
|
|
|
947
950
|
}
|
|
948
951
|
}
|
|
949
952
|
|
|
950
|
-
// —— project.yml:mode 枚举 ——
|
|
953
|
+
// —— project.yml:mode/phase 枚举 + 阶段就绪判据 ——
|
|
954
|
+
// 判据与 docs/spec.md §2 同源:phase 到 outline 及之后(setting 已过),核心设定件
|
|
955
|
+
// 必须在案——缺了说明写作流程被硬闯,报 error 提示回上一阶段补齐。
|
|
956
|
+
// phase 键缺失(无 project.yml 或旧文件)不查——stats 已提示「未设置」,此处不重复计账。
|
|
951
957
|
const projectRaw = parseFlatYamlArray(readTextOrNull(path.join(novelDir, 'project.yml')))
|
|
952
958
|
if (projectRaw !== null && !Array.isArray(projectRaw)) {
|
|
953
959
|
const mode = projectRaw.mode
|
|
@@ -955,6 +961,32 @@ export function checkProject(novelDir) {
|
|
|
955
961
|
push('error', 'project.yml', `mode 越枚举:${mode}(合法值 ${PROJECT_MODES.join('|')})`)
|
|
956
962
|
}
|
|
957
963
|
}
|
|
964
|
+
const phase = projectRaw !== null && !Array.isArray(projectRaw) ? projectRaw.phase : undefined
|
|
965
|
+
if (typeof phase === 'string' && phase !== '') {
|
|
966
|
+
if (!PROJECT_PHASES.includes(phase)) {
|
|
967
|
+
push('error', 'project.yml', `phase 越枚举:${phase}(合法值 ${PROJECT_PHASES.join('|')})`)
|
|
968
|
+
} else if (PROJECT_PHASES.indexOf(phase) >= PROJECT_PHASES.indexOf('outline')) {
|
|
969
|
+
if (!cards.some((c) => c.meta.role === 'protagonist')) {
|
|
970
|
+
push('error', 'characters/', 'phase 已过 setting 但 characters/ 缺 role: protagonist 主角卡(setting → outline 判据:核心角色卡须落盘)')
|
|
971
|
+
}
|
|
972
|
+
if (!cards.some((c) => c.meta.role === 'antagonist')) {
|
|
973
|
+
push('error', 'characters/', 'phase 已过 setting 但 characters/ 缺 role: antagonist 核心反派卡(setting → outline 判据:核心角色卡须落盘)')
|
|
974
|
+
}
|
|
975
|
+
const hasWorldLore =
|
|
976
|
+
listMdFiles(path.join(novelDir, 'world')).length > 0 || listMdFiles(path.join(novelDir, 'science')).length > 0
|
|
977
|
+
if (!hasWorldLore) {
|
|
978
|
+
push('error', 'world/', 'phase 已过 setting 但 world/ 与 science/ 均无条目(setting → outline 判据:世界观核心条目须落盘)')
|
|
979
|
+
}
|
|
980
|
+
if (readYamlArray(novelDir, 'volumes.yml').length === 0) {
|
|
981
|
+
push('error', 'volumes.yml', 'phase 已过 setting 但 volumes.yml 为空或缺失(setting → outline 判据:分卷规划须在案)')
|
|
982
|
+
}
|
|
983
|
+
// 关系网判据(六期补):两张及以上角色卡时 relations.yml 不得为空——
|
|
984
|
+
// 空关系网意味着设定阶段漏登关系,关系图与大纲师的戏剧权重都无从谈起。
|
|
985
|
+
if (cards.length >= 2 && readYamlArray(novelDir, 'relations.yml').length === 0) {
|
|
986
|
+
push('error', 'relations.yml', 'phase 已过 setting 但 relations.yml 为空(setting → outline 判据:两张及以上角色卡时关系网须登记)')
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
}
|
|
958
990
|
|
|
959
991
|
const summary = {
|
|
960
992
|
chapters: chapterFiles.length,
|
package/lib/panel-core.mjs
CHANGED
|
@@ -553,9 +553,18 @@ export function parseStateSnapshot(novelDir, options = {}) {
|
|
|
553
553
|
references: strArray(meta.references),
|
|
554
554
|
}))
|
|
555
555
|
|
|
556
|
-
const research = collectMdEntities(dir, 'research', yamlLib, warnings).map(({ file, meta }) => ({
|
|
556
|
+
const research = collectMdEntities(dir, 'research', yamlLib, warnings).map(({ file, meta, body }) => ({
|
|
557
557
|
file,
|
|
558
558
|
topic: strOrNull(meta.topic),
|
|
559
|
+
sources: strArray(meta.sources),
|
|
560
|
+
description: typeof body === 'string' ? body.trim() : '',
|
|
561
|
+
}))
|
|
562
|
+
|
|
563
|
+
const volumes = collectArrayYml(dir, 'volumes.yml', yamlLib, warnings).map((v) => ({
|
|
564
|
+
id: strOrNull(v.id),
|
|
565
|
+
title: strOrNull(v.title),
|
|
566
|
+
summary: strOrNull(v.summary),
|
|
567
|
+
chapters: strOrNull(v.chapters),
|
|
559
568
|
}))
|
|
560
569
|
|
|
561
570
|
const outlines = collectOutlines(dir, warnings)
|
|
@@ -581,6 +590,7 @@ export function parseStateSnapshot(novelDir, options = {}) {
|
|
|
581
590
|
timeline,
|
|
582
591
|
memories,
|
|
583
592
|
clues,
|
|
593
|
+
volumes,
|
|
584
594
|
world,
|
|
585
595
|
science,
|
|
586
596
|
research,
|