dsh-maoying 0.1.17 → 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 +3 -3
- package/README.md +3 -3
- package/docs/creator-panel.md +5 -2
- package/docs/design.md +2 -2
- package/docs/spec.md +3 -0
- package/index.mjs +17 -8
- package/lib/base.css +2 -1
- package/lib/client.js +48 -8
- package/lib/self-update.mjs +408 -123
- package/package.json +1 -1
- package/presets/maoying/agent.cordis.v2.yml +7 -7
- package/presets/maoying/agent.cordis.yml +7 -7
- package/skills/maoying-review-checklist/SKILL.md +8 -3
- package/templates/novel/README.md +4 -0
- package/templates/novel/style.md +35 -0
package/README.en.md
CHANGED
|
@@ -75,7 +75,7 @@ Open "**毛颖创作台**" at the bottom of the DSH sidebar — a full-page cock
|
|
|
75
75
|
|
|
76
76
|
- **Chapters**: status filters + the writing queue; click a chapter for full-page reading — back button, outline side-by-side (Markdown rendered), read-only manuscript, five narrative-dimension fields, one-click status flow.
|
|
77
77
|
- **Overview**: the "Edit" button on the manuscript card expands form-based editing of project.yml — title / author / genre / planned length / language / workflow phase / co-writing mode / synopsis (the former "Novel Settings" tab, merged in); saving preserves the file's existing comments. If project.yml was changed externally after the form loaded (manual edits or an agent advancing the phase), saving is rejected with a prompt to re-edit from the latest content instead of overwriting. A separate "Volume plan" card lists each volume's positioning and planned chapter range from volumes.yml — signed-off volume plans are visible in the panel.
|
|
78
|
-
- **Top bar**: current version always visible; a "new version ↗" badge links to the release page when npm has a newer one, next to an "Upgrade now" button that installs the latest release into the current DSH profile with pnpm (profile-directory installs only; a DeepSeek restart is required to take effect); the gear opens Settings — UI language (中文/English), six font-size steps (100%–150%), all applied immediately and remembered.
|
|
78
|
+
- **Top bar**: current version always visible; a "new version ↗" badge links to the release page when npm has a newer one, next to an "Upgrade now" button that installs the latest release into the current DSH profile with pnpm (profile-directory installs only, with an automatic corepack fallback when pnpm is missing; when a global `maoying` CLI is detected it is updated to the same exact version, and skipped if absent; a DeepSeek restart is required to take effect); the gear opens Settings — UI language (中文/English), six font-size steps (100%–150%), all applied immediately and remembered.
|
|
79
79
|
|
|
80
80
|
The panel only writes state (chapter status, project.yml, index.yml rebuilds), never prose — prose belongs to the author and the chief.
|
|
81
81
|
|
|
@@ -99,7 +99,7 @@ The plugin ships 4 web-novel methodology skills. At startup the installer drops
|
|
|
99
99
|
|---|---|
|
|
100
100
|
| maoying-golden-opening | Golden three chapters: role split of the first three chapters (world hook → motivation → conflict detonation), the "something happens in the first 300 words" rule, information-release discipline and a self-check list |
|
|
101
101
|
| maoying-beat-pacing | Payoff pacing: a payoff-type library, suppression-release ratios, chapter/arc/book pacing curves and chapter-end hook selection |
|
|
102
|
-
| maoying-review-checklist | Review checklist:
|
|
102
|
+
| maoying-review-checklist | Review checklist: seven layers (language / pacing / characterization / logic / foreshadowing / spoilers / style) with P0/P1/P2 severity |
|
|
103
103
|
| maoying-foreshadow-discipline | Foreshadow management: register on planting, pacing & scheduling, the "surprising yet reasonable" payoff bar, and ledger cleanup thresholds |
|
|
104
104
|
|
|
105
105
|
The installer only touches `maoying-*` prefixed directories; your own skills under `~/.dsh/skills/` are untouched, byte for byte. After uninstalling the plugin, remove the skill directories manually (all four together):
|
|
@@ -111,7 +111,7 @@ rm -rf "${DSH_HOME:-$HOME/.dsh}/skills/"maoying-*
|
|
|
111
111
|
|
|
112
112
|
## The novel/ Data Spec
|
|
113
113
|
|
|
114
|
-
The `novel/` directory is the plugin's entire database: project.yml (metadata + seven-phase state), volumes.yml, chapters/ (frontmatter five narrative dimensions + prose), characters/ (character cards), world/, science/ (three-color settings), relations.yml, foreshadows.yml (the state machine), timeline.yml, memories.yml (with appearance records and the promise ledger), clue-board.yml, research/, exports/. Full formats, examples and design rationale:
|
|
114
|
+
The `novel/` directory is the plugin's entire database: project.yml (metadata + seven-phase state), style.md (the writing style baseline), volumes.yml, chapters/ (frontmatter five narrative dimensions + prose), characters/ (character cards), world/, science/ (three-color settings), relations.yml, foreshadows.yml (the state machine), timeline.yml, memories.yml (with appearance records and the promise ledger), clue-board.yml, research/, exports/. Full formats, examples and design rationale:
|
|
115
115
|
|
|
116
116
|
- Quick reference: `novel/README.md` after project init (template source: [templates/novel/README.md](templates/novel/README.md))
|
|
117
117
|
- Full spec: [docs/spec.md](docs/spec.md) (why not SQLite, five-dimension field definitions, foreshadow state-machine semantics, appearance-record conventions)
|
package/README.md
CHANGED
|
@@ -75,7 +75,7 @@ DSH 左侧栏底部点「**毛颖创作台**」打开全页面板(改文件、
|
|
|
75
75
|
|
|
76
76
|
- **章节**:状态筛选 + 按单写作清单;点章节进整页阅读——返回按钮、章纲对照(Markdown 渲染)、只读正文、五维叙事字段、一键状态流转。
|
|
77
77
|
- **概览**:书稿卡右上「编辑」按钮展开 project.yml 的书名/作者/题材/篇幅/语言/工作流阶段/共创模式/简介表单化编辑(原「小说设置」tab 并入),保存保留文件里原有注释;表单载入后 project.yml 被外部(作者手改 / agent 推进阶段)改过时拒绝保存并提示重改,不覆盖人工改动。概览另设「分卷规划」卡,列出 volumes.yml 各卷定位与计划章节范围——拍板过的分卷有处可查。
|
|
78
|
-
- **顶栏**:当前版本号常驻;npm 上有新版时亮「有新版 ↗」徽章直连发布页,旁边「一键升级」按钮直接用 pnpm 把最新包装进当前 DSH 档(仅支持 profile
|
|
78
|
+
- **顶栏**:当前版本号常驻;npm 上有新版时亮「有新版 ↗」徽章直连发布页,旁边「一键升级」按钮直接用 pnpm 把最新包装进当前 DSH 档(仅支持 profile 目录安装,pnpm 缺失时自动回落 corepack;检测到全局 `maoying` CLI 时顺带以同一精确版本更新,未装则跳过;装完需重启 DeepSeek 才生效);齿轮打开系统设置,界面语言中文/English 可切、字号六档(100%–150%)整体缩放,均立即生效并记忆。
|
|
79
79
|
|
|
80
80
|
面板只写状态(章节状态、project.yml、index.yml 重建),不改正文——正文永远归作者与主编。
|
|
81
81
|
|
|
@@ -99,7 +99,7 @@ author-draft 下的协作循环——你只管写,交稿即接管杂务:
|
|
|
99
99
|
|---|---|
|
|
100
100
|
| maoying-golden-opening | 黄金三章:前三章职能分工(世界钩子 → 动机确立 → 冲突引爆)、前 300 字出事原则、信息释放纪律与自检清单 |
|
|
101
101
|
| maoying-beat-pacing | 爽点节奏:爽点类型库、压抑-释放配比、单章/中/大三级节奏曲线与章尾钩子选型 |
|
|
102
|
-
| maoying-review-checklist |
|
|
102
|
+
| maoying-review-checklist | 审校清单:语言/节奏/人设/逻辑/伏笔/剧透/风格七层逐项检查,按 P0/P1/P2 三档严重度输出 |
|
|
103
103
|
| maoying-foreshadow-discipline | 伏笔管理:埋设即登记、推进节奏排期、回收「意外 + 合理」标准与台账清理阈值 |
|
|
104
104
|
|
|
105
105
|
安装器只认 `maoying-*` 前缀目录,`~/.dsh/skills/` 下你自己的技能一个字节不动。卸载插件后技能目录需手动清理(四目录一起删):
|
|
@@ -111,7 +111,7 @@ rm -rf "${DSH_HOME:-$HOME/.dsh}/skills/"maoying-*
|
|
|
111
111
|
|
|
112
112
|
## novel/ 数据规范
|
|
113
113
|
|
|
114
|
-
`novel/` 目录是整个插件的数据库:project.yml(元信息与七阶段 phase)、volumes.yml、chapters/(frontmatter 五维叙事字段 + 正文)、characters/(角色卡)、world/、science/(三色设定)、relations.yml、foreshadows.yml(伏笔状态机)、timeline.yml、memories.yml(含出场记录与承诺账)、clue-board.yml、research/、exports/。全部格式、示例与设计理由见:
|
|
114
|
+
`novel/` 目录是整个插件的数据库:project.yml(元信息与七阶段 phase)、style.md(写作风格基准)、volumes.yml、chapters/(frontmatter 五维叙事字段 + 正文)、characters/(角色卡)、world/、science/(三色设定)、relations.yml、foreshadows.yml(伏笔状态机)、timeline.yml、memories.yml(含出场记录与承诺账)、clue-board.yml、research/、exports/。全部格式、示例与设计理由见:
|
|
115
115
|
|
|
116
116
|
- 速查版:项目初始化后的 `novel/README.md`(模板源 [templates/novel/README.md](templates/novel/README.md))
|
|
117
117
|
- 完整版:[docs/spec.md](docs/spec.md)(为什么不用 SQLite、五维字段定义、伏笔状态机语义、出场记录约定)
|
package/docs/creator-panel.md
CHANGED
|
@@ -39,7 +39,7 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
39
39
|
|---|---|---|---|
|
|
40
40
|
| `client.css` | GET | — | text/css(lib/base.css 现读,no-cache) |
|
|
41
41
|
| `update-check` | GET | — | `{ ok, current, latest, updateAvailable, releaseUrl, checkedAt, error, source }`——新版检测(npm registry → npmmirror → GitHub 三级源,1h 缓存 + 7 天 stale-if-error,全败静默降级为 latest=null / error 非空) |
|
|
42
|
-
| `self-update` | POST | — | `{ ok, from, to, restartRequired: true }` 或 `{ ok: false, kind, error }
|
|
42
|
+
| `self-update` | POST | — | `{ ok: true, kind: 'installed', from, to, restartRequired: true }` / `{ ok: true, kind: 'up-to-date', from }` 或 `{ ok: false, kind, error }`——面板内一键升级:安装形态 + profile 就绪预检 + 版本预检(registry 失败再 mirror,拿不到 latest 不动手)通过后,在 profile 目录里直跑 `pnpm add dsh-maoying@<精确版本>`(pnpm 候选链逐个 spawn:裸 pnpm → corepack pnpm → homebrew / node@XX keg-only / execPath 同目录 corepack → PNPM_HOME 落位,仅 ENOENT 换下一路;180s 超时)。仅支持 `<dshHome>/profiles/<profile>/node_modules/dsh-maoying` 真实目录安装;symlink/dev 检出(dev-link)、非 profile 布局(unsupported-layout)、profile 未初始化(profile-missing)、拿不到最新版(no-target)、pnpm 缺失(pnpm-missing,message 带手动出路)、超时(timeout)、pnpm 非零退出(nonzero)一律 200 + ok:false。**装完只换磁盘包,运行中进程仍是旧代码,restartRequired 恒 true——重启 DeepSeek 后生效** |
|
|
43
43
|
| `host-debug` | GET | — | `{ ok, hostVersion, trace, shape, registry, runtime }`——现场诊断(见下节) |
|
|
44
44
|
| `workspaces` | GET | — | `{ ok, workspaces: [{ path, name, hasNovel }] }`——workspaceRegistry.list() 逐根探测 `<root>/novel/project.yml` |
|
|
45
45
|
| `state` | GET | `path=<root>` | 全量快照(见 §3 契约) |
|
|
@@ -137,7 +137,10 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
137
137
|
- **按单写作兼容孤儿章纲**(0.1.15):章纲先于正文时面板不再零入口——「按单写作」区块在联交(有章纲且 status=pending 的章)之外照列未建章的孤儿章纲(按章纲序保序混排):「未建章」徽章 + 章纲节选 + 字数 + 两个动作(行点击开章纲全文弹层:MaskPortal + MdView 渲染 GET `outline` 的 body、words 一并显示;「建章」按钮 POST `chapter-create` 在 chapters/ 落 status=pending 空章——frontmatter 按 chapters/vXX-cXXX.md 模板、title 取章纲首条标题行去「章纲」后缀,成功 toast 后重拉快照,新章进表格、走既有 pending 流转,孤儿行按新快照自然消失);chapters/ 为空而 outlines/ 非空时空态改提示「下方按单写作已列出未建章章纲」。路由 13 → 15 条(outline GET / chapter-create POST);I18N zh/en 各 219 → 230 条(chapters.outlineRowTip / noChapter / createBtn / creating / createTip / emptyWithOutlines、toast.chapterCreated、outline.modalAria / closeAria / loading / loadFailed)。
|
|
138
138
|
- **小说设置并入概览**(0.1.15):原第 9 个「小说设置」tab 撤销(八个 tab),改为概览书稿卡右上「编辑」按钮——点击后 hero 卡原位切换为书稿元信息表单(ProjectMetaForm,8 平键 + 取消/保存),保存链路不变(`POST project-save` 携 `base_hash` 409 人工改动保护,成功重拉快照并退出编辑态,取消直接退出不落盘)。路由不变;I18N zh/en 各 230 → 232 条(tabs.settings 移除,overview.edit / overview.editTip / novel.cancel 新增)。
|
|
139
139
|
- **role 徽章本地化**(0.1.15):角色卡墙与全卡弹层的 role 徽章不再直出角色卡 frontmatter 原文——契约四态(`protagonist|antagonist|supporting|minor`)按界面语言出标签(主角/反派/配角/龙套;英文面板 Protagonist/Antagonist/Supporting/Minor),role 是自由文本、契约外取值原样展示不翻译;tone 归色逻辑(roleTone 关键词匹配)不变。I18N zh/en 各 232 → 236 条(role.protagonist / antagonist / supporting / minor)。
|
|
140
|
-
- **分卷规划卡 +
|
|
140
|
+
- **分卷规划卡 + 考据笔记正文**(0.1.18):快照顶层新增 `volumes`(volumes.yml 原样:id/title/summary/chapters 计划范围串)——概览新增「分卷规划」卡(有卷才显,卷头 id · 卷名 + 计划范围 tag + 卷定位一句),分卷拍板件在面板有复核位(配合 CLI check 的 phase 阶段就绪判据);research 快照从只取 topic 扩为 sources + description,设定 tab「资料检索」从标签行改为逐条渲染:topic 标题 + 笔记正文(MdView)+ sources 参考(悬停显全文 URL)。路由不变;I18N zh/en 各 +1 条(overview.volumes)。
|
|
141
|
+
- **pnpm 候选链对齐 dsh-paoding**(0.1.18):修复一键升级「未找到 pnpm」误报。病因(实机验证):本机 pnpm 由 corepack 管理但未 `corepack enable`,PATH 里没有裸 pnpm,PNPM_HOME 落位也没有——旧版 existsSync+accessSync(X_OK) 文件预筛(PATH 目录扫描 + homebrew/pnpm 官方落位)全落空直接报 pnpm-missing;且 desktop Electron 进程 PATH 可能是 launchd 最小集(实测最小 PATH 下裸 corepack 也 ENOENT),`/opt/homebrew/bin/corepack` 不在,只有 `/opt/homebrew/opt/node@24/bin/corepack` 在盘上。机制照 dsh-paoding tools/lib/upgrade.mjs 对齐:① **spawn ENOENT 探测链**替代文件预筛——逐候选真 spawn,仅 err.code==='ENOENT' 换下一路(EACCES / 超时 / 非零退出是真实运行结果,如实返回不换通道),绝对路径候选才 existsSync 预筛(不存在不浪费 spawn);新候选链:裸 pnpm → 裸 corepack(+`pnpm` 参数前缀)→ `/opt/homebrew/bin/corepack` → `/opt/homebrew/opt/node@XX/bin/corepack`(readdirSync 过滤 node 前缀目录拼路径,多版本按序全收;目录不存在跳过)→ execPath 同目录 corepack(桌面 Electron 下无用但无害,headless/web profile 有用)→ `~/Library/pnpm`、`~/.local/share/pnpm`。② **升级前版本预检**:fetchLatestFromRegistry(10s) 失败再 fetchLatestFromMirror(10s),拿不到 latest 不做半截升级(kind: no-target,message 带两路错误);compareVersions(target, current)<=0 短路 kind: 'up-to-date'(`{ ok: true, kind: 'up-to-date', from }`,前端 toast「已是最新版」后按钮恢复可点、不进已安装态);装精确版本 `dsh-maoying@<target>` 而非 @latest(避开 pnpm 新鲜度防护 / 镜像 packument 滞后的静默空转)。③ **cwd = profile 目录**(pnpm 操作对象就是它),去掉 --dir。④ profile 目录缺 package.json → 诚实失败 kind: 'profile-missing'。⑤ win32 pnpm/corepack 是 .cmd shim:spawn shell:true(CVE-2024-27980 加固)+ 命令补 .cmd 后缀、参数保持数组;darwin/linux 恒 shell:false。⑥ 子进程输出尾部 2KB → 8KB(合并 stdout+stderr);全候选 ENOENT 的 pnpm-missing message 带可操作出路:「可先在终端执行 corepack enable 并重启 DSH 后重试;或手动升级:cd <profileDir> && corepack pnpm add dsh-maoying@<目标版本>」,dev-link 的 message 保留 git pull 出路。spawnImpl 契约改 paoding 形状 `(command, args, options, timeoutMs, timeoutLabel) → { code, output, error?, errorCode? }`(缺省 defaultSpawn 合并输出、超时 SIGTERM、5s 宽限后 SIGKILL 兜底、收敛 errorCode 'ETIMEDOUT');退出码非零 kind 从 pnpm-failed 更名 nonzero。前端 pnpm-missing 等失败 message 服务端已带出路文案,toast 直显不截断。安全闸(realpath 形态两道闸 / 模块级 in-flight 去重 / 成功后 invalidateVersionCache)与依赖全注入风格不变。路由仍 15 条;I18N zh/en 各 237 → 238 条(toast.selfUpdate.upToDate)。
|
|
142
|
+
- **概览补显共创模式**(0.1.18):书稿卡阶段行 phase pill 后补「共创模式」短标签徽章(悬浮出全称;快照 mode 缺失或越枚举时不显示,老项目无该键不误标)。路由不变;I18N zh/en 各 238 → 241 条(modeShort.aiDraft / coWrite / authorDraft)。
|
|
143
|
+
- **一键升级顺带全局 CLI**(0.1.18):本机常见两份独立安装——desktop profile(`<dshHome>/profiles/desktop/node_modules/dsh-maoying`,一键升级的更新对象)与全局 CLI(`npm i -g` 装的 `<npm 全局>/node_modules/dsh-maoying`,如 `/opt/homebrew/bin/maoying` → `/opt/homebrew/lib/node_modules/dsh-maoying/bin/maoying.mjs`)——只更 profile 会版本偏斜;方案 B:profile 阶段落定(installed 装完 / up-to-date 短路)后跑独立全局阶段,把两份收敛到同一精确版本。**检测**(detectGlobalCli,零 spawn 纯文件系统):PATH 各目录 + `/opt/homebrew/bin` + `/usr/local/bin` 找 `maoying`(win32 `maoying.cmd`),existsSync 预筛;命中后 realpathSync 该 bin 两级上溯得包根(`bin/maoying.mjs` → 包根),包根 package.json 的 name 必须是 `dsh-maoying` 才认(防怪链接);包根 realpath 落在本 profile 目录内(如 profile 自带 `.bin/maoying`)→ 不算全局,跳过该候选继续找;全落空 → 不 spawn。**更新**走 npm 候选链(与 pnpm 链同款 spawn ENOENT 探测):裸 npm → execPath 同目录 → `/opt/homebrew/bin` → `/usr/local/bin` → `/opt/homebrew/opt/node@XX/bin`,spawn `npm i -g dsh-maoying@<target>`(cwd=os.tmpdir()、独立 120s 超时,profile pnpm 仍 180s)。**返回形状**:成功对象新增 `globalCli` 字段——`{ ok: true, skipped: 'missing' }`(没装全局,跳过)/ `{ ok: true, skipped: 'same', version }`(已同版)/ `{ ok: true, from, to }` / `{ ok: false, error, output? }`(error 中文、含手动出路 `npm i -g dsh-maoying@<target>`)——**全局失败不拖累 profile 安装**:整体仍 ok:true,重启照常生效,失败如实透出;dev-link / unsupported-layout / no-target / profile-missing 等失败路径维持整单拒绝,不碰全局。路由把 globalCli 原样透传(up-to-date 与 installed 两个成功分支)。前端成功 toast 主文案不变、按 globalCli 拼第二句(missing / same 不提零噪音,真更新了显「全局 maoying CLI a → b」,失败如实报错),up-to-date 分支同样拼接,toast 文本容器加 `white-space: pre-line` 承接换行;self-update 请求前端超时 200s → 340s(180 + 120 + 40 余量)。路由不变;I18N zh/en 各 241 → 243 条(toast.selfUpdate.globalOk / globalFail)。
|
|
141
144
|
|
|
142
145
|
## 5. 验收基线
|
|
143
146
|
|
package/docs/design.md
CHANGED
|
@@ -133,13 +133,13 @@ delegation-labels 静态角色名映射:{outline: 大纲师, write: 写手, re
|
|
|
133
133
|
persona prefix 内容骨架(agent.cordis.yml 内联,中文书写):
|
|
134
134
|
|
|
135
135
|
1. **身份**:网文主编/创作总监,管 project 目录 novel/,重活委派五帮手,自己只做规划、衔接、验收、状态推进。
|
|
136
|
-
2. **开工纪律**:先读 novel/project.yml + index.yml
|
|
136
|
+
2. **开工纪律**:先读 novel/project.yml + index.yml + style.md(若有);不确定文件格式读 novel/README.md;不全量读章节,写到哪章读哪章上下文(前章结尾 ~1500 字 + 该章 frontmatter + 出场角色卡 + 活跃伏笔节选)。
|
|
137
137
|
3. **七阶段状态机**(每阶段推进写 project.yml phase):idea 选题 → setting 设定 → outline 大纲 → writing 写作 → review 审阅 → consistency 一致性 → export 导出。每完成一个里程碑必须推进 phase 并告知用户。
|
|
138
138
|
4. **Writing 六步法**(逐章循环):
|
|
139
139
|
① 衔接检查:读前章结尾,确认无断裂;
|
|
140
140
|
② 委派 outline 出/改本章大纲(含五维字段),主编过目;
|
|
141
141
|
③ 委派 write 写正文(写手人设要求:按大纲覆盖五维、不写崩人设、章尾留钩子),status→writing;
|
|
142
|
-
④ 委派 review
|
|
142
|
+
④ 委派 review 审稿(清单:语言/节奏/人设符合/不剧透/伏笔推进合理/前后矛盾/风格符合),status→review;
|
|
143
143
|
⑤ 审稿问题回 write 修(continuable 就地续修)或主编微调;
|
|
144
144
|
⑥ 通过后 status→accepted,委派 lore 做定稿维护(出场记录、伏笔状态、记忆、线索板、时间线),`maoying index` 重建概览。
|
|
145
145
|
循环检查:还有非 accepted 章节 → 回 ①。
|
package/docs/spec.md
CHANGED
|
@@ -29,6 +29,9 @@
|
|
|
29
29
|
### project.yml
|
|
30
30
|
`phase` 七态 `idea|setting|outline|writing|review|consistency|export` 只能由主编推进(帮手不得越权改),且必须顺序推进不可跳越;推进时主编须向用户简报。推进有完成判据:setting → outline 须核心角色卡(主角、核心反派)与世界观核心条目(金手指规则、对抗格局)已落盘,且 characters/ 有两张及以上角色卡时 relations.yml 不为空(关系网须登记);outline → writing 须 volumes.yml 在案且目标章大纲已出;判据不满足回上一阶段补齐。冷启动(Collab 四步)在设定落盘与分卷规划两处各设一个用户拍板点——主编简报列决策点,等用户实质确认才进下一步。`language` 影响写手与审校的语言纪律(如 `zh` 不允许成段外文)。`mode` 三值决定正文归谁写:`ai-draft`(缺省,AI 代笔,主编走 Writing 六步法)/ `co-write`(逐章指派谁写,主编动笔前先问清本章归谁)/ `author-draft`(全书作者亲笔,主编只备料、审稿、记账,永不代写)。文件里没有 `mode` 键的旧项目一律按 `ai-draft` 处理。
|
|
31
31
|
|
|
32
|
+
### style.md
|
|
33
|
+
写作风格基准(人称视角、语言基调、节奏、对话、情绪与爽点、禁忌),写手/大纲师/审校一律遵守;纯 markdown、无 frontmatter,作者随时手改、改完即生效;冷启动由主编与用户对齐后落盘,旧项目缺失时按通用网文基线执行。设计理由:风格集中在一个可手改文件里,而不是写死在 persona——persona 只写加载纪律(读它、服从它、冲突上报主编),风格内容随书可变,作者不改插件就能改风格;旧项目没有该文件时优雅降级(各 persona 与审校风格层均写「若有」);审校风格层以其为裁判基准,文件缺席时退回通用网文基线抽查明显风格跳变,并在报告注明「无风格基准」。
|
|
34
|
+
|
|
32
35
|
### volumes.yml
|
|
33
36
|
`id` 与章节文件名前缀严格一致(`v01` ↔ `chapters/v01-cXXX.md`),index.yml 的卷分组按本文件顺序输出。`chapters` 范围写法 `001-020` 是给人看的计划值,运行时以章节文件实况为准。
|
|
34
37
|
|
package/index.mjs
CHANGED
|
@@ -996,16 +996,25 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
996
996
|
}
|
|
997
997
|
|
|
998
998
|
if (route === 'self-update' && req.method === 'POST') {
|
|
999
|
-
// 面板内一键升级:形态/
|
|
1000
|
-
// dsh-maoying
|
|
1001
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
999
|
+
// 面板内一键升级:形态/profile 预检 + 版本预检后,在 profile 目录里
|
|
1000
|
+
// 直跑 `pnpm add dsh-maoying@<精确版本>`(pnpm 按候选链解析,细节与
|
|
1001
|
+
// 安全边界见 lib/self-update.mjs)。可预期的失败(dev-link /
|
|
1002
|
+
// unsupported-layout / profile-missing / no-target / pnpm-missing /
|
|
1003
|
+
// 超时 / 退出码非零 nonzero)统一 HTTP 200 + ok:false,kind 给客户端
|
|
1004
|
+
// 分流、message 经 sanitizeMessage 脱敏后透出;成功带 restartRequired:
|
|
1004
1005
|
// true——装完的只是磁盘上的新包,运行中的进程仍是旧代码,必须重启
|
|
1005
|
-
// DeepSeek
|
|
1006
|
+
// DeepSeek 才生效,客户端文案必须照实转述。本地已不落后线上时短路
|
|
1007
|
+
// kind: 'up-to-date'(不装 profile)。两个成功路径都顺带全局 CLI:
|
|
1008
|
+
// globalCli 原样透传(json 序列化不截字段;形状与失败不拖累整体的
|
|
1009
|
+
// 语义见 lib/self-update.mjs 文件头注「全局 CLI 顺带更新」节)。
|
|
1006
1010
|
const r = await selfUpdate({ logger: ctx.logger })
|
|
1007
|
-
if (r.ok) {
|
|
1008
|
-
json(res, 200, { ok: true, from: r.from ?? null,
|
|
1011
|
+
if (r.ok && r.kind === 'up-to-date') {
|
|
1012
|
+
json(res, 200, { ok: true, kind: 'up-to-date', from: r.from ?? null, globalCli: r.globalCli ?? null })
|
|
1013
|
+
} else if (r.ok) {
|
|
1014
|
+
json(res, 200, {
|
|
1015
|
+
ok: true, kind: r.kind, from: r.from ?? null, to: r.to ?? null,
|
|
1016
|
+
restartRequired: true, globalCli: r.globalCli ?? null,
|
|
1017
|
+
})
|
|
1009
1018
|
} else {
|
|
1010
1019
|
json(res, 200, { ok: false, kind: r.kind, error: sanitizeMessage(r.message ?? r.kind) })
|
|
1011
1020
|
}
|
package/lib/base.css
CHANGED
|
@@ -709,7 +709,8 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
|
|
|
709
709
|
box-shadow: 0 8px 28px color-mix(in srgb, #000 24%, transparent);
|
|
710
710
|
pointer-events: auto;
|
|
711
711
|
}
|
|
712
|
-
|
|
712
|
+
/* pre-line:一键升级 toast 的全局 CLI 第二句以 \n 换行显示(client.js globalCliNote)。 */
|
|
713
|
+
.myt-toastText { min-width: 0; max-height: 120px; overflow-y: auto; overflow-wrap: anywhere; white-space: pre-line; }
|
|
713
714
|
.myt-toastOk { border-color: color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent); }
|
|
714
715
|
.myt-toastWarn { border-color: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent); }
|
|
715
716
|
.myt-toastErr { border-color: color-mix(in srgb, var(--dsw-alias-state-error-primary) 50%, transparent); }
|
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 写,红鲱鱼/推理两组永远查不中、只能裸显英文原值。
|
|
@@ -696,24 +699,48 @@ window.__ModuleLoader__.load({
|
|
|
696
699
|
}, []);
|
|
697
700
|
// 一键升级状态机:""(可点)→ "installing"(安装中,按钮禁用)→
|
|
698
701
|
// "installed"(已装完待重启,按钮永久禁用、新版徽章收起)。可预期失败
|
|
699
|
-
// (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
|
+
}
|
|
700
719
|
var _up = useState(""), upState = _up[0], setUpState = _up[1];
|
|
701
720
|
function runSelfUpdate() {
|
|
702
721
|
if (upState !== "") return;
|
|
703
722
|
setUpState("installing");
|
|
704
723
|
// 不走 apiPost:成功提示要带新版本号、失败按 kind 分流文案,且超时
|
|
705
|
-
// 单独放宽(服务端 pnpm 最长 180s
|
|
724
|
+
// 单独放宽(服务端 profile pnpm 最长 180s + 全局 npm 独立 120s,统一
|
|
725
|
+
// 60s 会先掐断,见常量处注释)。
|
|
706
726
|
maoyingApi("self-update", {}, SELF_UPDATE_TIMEOUT_MS).then(function (d) {
|
|
707
|
-
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) {
|
|
708
733
|
setUpState("installed");
|
|
709
734
|
// 硬约束照实说:装完只是磁盘换新,运行中的进程仍是旧代码。
|
|
710
|
-
toast.ok(tf("toast.selfUpdate.ok", { ver: (d && d.to) || "?" }));
|
|
735
|
+
toast.ok(tf("toast.selfUpdate.ok", { ver: (d && d.to) || "?" }) + globalCliNote(d));
|
|
711
736
|
} else if (d && d.kind === "dev-link") {
|
|
712
737
|
setUpState("");
|
|
713
738
|
toast.warn(tt("toast.selfUpdate.devLink"));
|
|
714
739
|
} else if (d && d.kind === "pnpm-missing") {
|
|
740
|
+
// 服务端 message 已带可操作出路(corepack enable / 手动命令),直显不截断;
|
|
741
|
+
// 拿不到 message 才回落本地通用文案。
|
|
715
742
|
setUpState("");
|
|
716
|
-
toast.
|
|
743
|
+
toast.err(tf("toast.prefix", { msg: (d && d.error) || tt("toast.selfUpdate.pnpmMissing") }));
|
|
717
744
|
} else {
|
|
718
745
|
setUpState("");
|
|
719
746
|
toast.err(tf("toast.prefix", { msg: (d && d.error) || tt("common.unknownErr") }));
|
|
@@ -880,6 +907,9 @@ window.__ModuleLoader__.load({
|
|
|
880
907
|
var _b = useState(false), phaseBusy = _b[0], setPhaseBusy = _b[1];
|
|
881
908
|
var phaseIdx = PHASES.indexOf(project.phase);
|
|
882
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; } }
|
|
883
913
|
// 静态体检黄条:仅 found 时拉 check 路由(未初始化没有可查的东西)。
|
|
884
914
|
// 工作区切换与每次 reload 都换新 snap → effect 重跑即重查;请求序号防
|
|
885
915
|
// 过期响应(快速切工作区时在途旧响应不得落进新工作区的黄条)。
|
|
@@ -920,6 +950,10 @@ window.__ModuleLoader__.load({
|
|
|
920
950
|
h("h2", { className: "myt-heroTitle", key: "title" }, orDash(project.title)),
|
|
921
951
|
h("div", { className: "myt-heroMeta", key: "phase" },
|
|
922
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,
|
|
923
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)),
|
|
924
958
|
project.description ? h("p", { className: "myt-heroDesc", key: "desc" }, project.description) : null,
|
|
925
959
|
]),
|
|
@@ -1084,6 +1118,8 @@ window.__ModuleLoader__.load({
|
|
|
1084
1118
|
"header.upgradeTitle": "用 pnpm 安装 npm 最新版(最长约 3 分钟),装完需重启 DeepSeek 生效",
|
|
1085
1119
|
"header.upgradedTitle": "已安装新版,重启 DeepSeek 后生效",
|
|
1086
1120
|
"toast.selfUpdate.ok": "已安装 {ver},重启 DeepSeek 后生效",
|
|
1121
|
+
"toast.selfUpdate.upToDate": "已是最新版({ver})",
|
|
1122
|
+
"toast.selfUpdate.globalOk": "全局 maoying CLI {from} → {to}", "toast.selfUpdate.globalFail": "全局 CLI 更新失败:{error}",
|
|
1087
1123
|
"toast.selfUpdate.devLink": "开发目录安装,请 git pull 更新", "toast.selfUpdate.pnpmMissing": "未找到 pnpm,请手动安装",
|
|
1088
1124
|
"header.workspace": "工作区", "header.wsAria": "选择工作区", "header.noNovel": "(无 novel)", "header.untitled": "未命名",
|
|
1089
1125
|
"header.settings": "系统设置", "header.refresh": "重新拉取", "header.closeAria": "关闭创作台", "header.closeTitle": "关闭",
|
|
@@ -1155,6 +1191,7 @@ window.__ModuleLoader__.load({
|
|
|
1155
1191
|
"novel.lengthPh": "如:约100万字 / 300章", "novel.fLanguage": "正文语言", "novel.fPhase": "工作流阶段", "novel.fMode": "共创模式",
|
|
1156
1192
|
"novel.fDesc": "一句话简介", "novel.descPh": "写封面文案时可直接引用", "novel.titleRequired": "书名不能为空",
|
|
1157
1193
|
"mode.aiDraft": "AI 代笔(AI 写正文)", "mode.coWrite": "逐章共创(按章指派谁写)", "mode.authorDraft": "作者亲笔(AI 只备料审稿)",
|
|
1194
|
+
"modeShort.aiDraft": "AI 代笔", "modeShort.coWrite": "逐章共创", "modeShort.authorDraft": "作者亲笔",
|
|
1158
1195
|
"novel.saving": "保存中…", "novel.save": "保存设置", "novel.cancel": "取消",
|
|
1159
1196
|
"settings.title": "系统设置", "settings.closeAria": "关闭设置", "settings.language": "语言",
|
|
1160
1197
|
"settings.languageHint": "界面文字随语言选项切换,立即生效并记忆。", "settings.fontScale": "字号",
|
|
@@ -1170,6 +1207,8 @@ window.__ModuleLoader__.load({
|
|
|
1170
1207
|
"header.upgradeTitle": "Install the latest npm release with pnpm (up to ~3 min); takes effect after restarting DeepSeek",
|
|
1171
1208
|
"header.upgradedTitle": "New version installed — takes effect after restarting DeepSeek",
|
|
1172
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}",
|
|
1173
1212
|
"toast.selfUpdate.devLink": "Installed from a dev directory — update with git pull", "toast.selfUpdate.pnpmMissing": "pnpm not found — install it manually",
|
|
1174
1213
|
"header.workspace": "Workspace", "header.wsAria": "Select workspace", "header.noNovel": "(no novel)", "header.untitled": "Untitled",
|
|
1175
1214
|
"header.settings": "Settings", "header.refresh": "Refresh", "header.closeAria": "Close Maoying Studio", "header.closeTitle": "Close",
|
|
@@ -1241,6 +1280,7 @@ window.__ModuleLoader__.load({
|
|
|
1241
1280
|
"novel.lengthPh": "e.g. ~1M characters / 300 chapters", "novel.fLanguage": "Text language", "novel.fPhase": "Workflow phase", "novel.fMode": "Co-writing mode",
|
|
1242
1281
|
"novel.fDesc": "One-line synopsis", "novel.descPh": "Can be quoted directly in cover copy", "novel.titleRequired": "Title is required",
|
|
1243
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",
|
|
1244
1284
|
"novel.saving": "Saving…", "novel.save": "Save", "novel.cancel": "Cancel",
|
|
1245
1285
|
"settings.title": "Settings", "settings.closeAria": "Close settings", "settings.language": "Language",
|
|
1246
1286
|
"settings.languageHint": "UI text follows the language option; it applies immediately and is remembered.", "settings.fontScale": "Font size",
|
package/lib/self-update.mjs
CHANGED
|
@@ -1,42 +1,108 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 面板内一键升级 ——
|
|
2
|
+
* 面板内一键升级 —— 版本预检 + pnpm 候选链(机制对齐 dsh-paoding
|
|
3
|
+
* tools/lib/upgrade.mjs,0.1.18 起)。
|
|
3
4
|
*
|
|
4
5
|
* 背景:desktop 档的真实安装形态是 <dshHome>/profiles/<profile>/node_modules/
|
|
5
6
|
* dsh-maoying(真实目录,非 symlink),宿主 dsh CLI 拒绝管理 desktop 档,唯一
|
|
6
|
-
*
|
|
7
|
+
* 可行的升级命令是在 profile 目录里直跑 `pnpm add dsh-maoying@<精确版本>`:
|
|
8
|
+
* - cwd 就是 profile 目录(pnpm 的操作对象是它),不带 --dir;
|
|
9
|
+
* - 装精确版本而非 @latest——避开 pnpm 新鲜度防护 / 镜像 packument 滞后
|
|
10
|
+
* 造成的「跑了但啥也没装」静默空转。
|
|
11
|
+
*
|
|
12
|
+
* pnpm 解析(0.1.18 重写,修复「未找到 pnpm」误报):不再 existsSync+
|
|
13
|
+
* accessSync(X_OK) 文件预筛(corepack 管理且未 `corepack enable` 的机器 PATH
|
|
14
|
+
* 里没有裸 pnpm,桌面 GUI 进程 PATH 还可能是 launchd 最小集,预筛必落空),
|
|
15
|
+
* 改为 spawn ENOENT 探测链:逐候选真 spawn,仅 err.code==='ENOENT' 才换下一
|
|
16
|
+
* 路;EACCES / 超时 / 非零退出都是真实运行结果,如实返回不换通道。候选链:
|
|
17
|
+
* 1) 裸 pnpm(npm i -g / homebrew 等常规安装);
|
|
18
|
+
* 2) 裸 corepack(corepack 随 node 发行,未 enable 也能 `corepack pnpm ...`
|
|
19
|
+
* 即装即用);
|
|
20
|
+
* 3) /opt/homebrew/bin/corepack(existsSync 预筛,不在盘上不浪费 spawn);
|
|
21
|
+
* 4) /opt/homebrew/opt/node@XX/bin/corepack(readdirSync 过滤 node 前缀目录
|
|
22
|
+
* 拼路径,
|
|
23
|
+
* 兜住 keg-only 的 node@XX 版代安装——desktop Electron 最小 PATH 实测
|
|
24
|
+
* 全靠它命中);
|
|
25
|
+
* 5) path.dirname(process.execPath)/corepack(桌面 Electron 下无用但无害,
|
|
26
|
+
* headless / web profile 的 node 同目录形态有用);
|
|
27
|
+
* 6) ~/Library/pnpm、~/.local/share/pnpm(pnpm 独立安装脚本落位)。
|
|
28
|
+
* win32 的 pnpm / corepack 都是 .cmd shim,spawn 必须 shell:true(Node 直接
|
|
29
|
+
* spawn .cmd 会 EINVAL,见 CVE-2024-27980 加固),命令补 .cmd 后缀、参数保持
|
|
30
|
+
* 数组形式;darwin/linux 恒 shell:false,无注入面。
|
|
31
|
+
*
|
|
32
|
+
* 升级前版本预检:fetchLatestFromRegistry(10s)失败再 fetchLatestFromMirror
|
|
33
|
+
* (10s),拿不到 latest 绝不做半截升级;compareVersions(target, current) <= 0
|
|
34
|
+
* 直接短路「已是最新」,不 spawn。装完失效版本检测缓存。
|
|
7
35
|
*
|
|
8
36
|
* 安全边界(缺一不可,全不满足绝不执行安装):
|
|
9
37
|
* 1) 安装形态判定——插件包自身目录先 realpathSync 取真路径,真路径 ≠ 常规
|
|
10
38
|
* 路径即 link/symlink 安装(如 web 档指向 dev repo)→ 拒绝(dev-link,
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* 2)
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
39
|
+
* 指路 git pull);真路径不匹配 <dshHome>/profiles/<profile>/node_modules/
|
|
40
|
+
* dsh-maoying 结构 → 拒绝(unsupported-layout)。dshHome / profile 取
|
|
41
|
+
* env.DSH_HOME / DSH_PROFILE(~ 前缀手动展开),缺失时回退从真路径的
|
|
42
|
+
* profiles 祖先推导,推导不出同样拒绝。
|
|
43
|
+
* 2) profile 就绪判定——profile 目录没有 package.json(未初始化)→ 拒绝
|
|
44
|
+
* (profile-missing),不硬跑 pnpm(会在空目录另起一份 package.json)。
|
|
45
|
+
* 3) 超时 180 秒——超时 SIGTERM(5s 后升级 SIGKILL)收敛为 timeout,不挂死
|
|
46
|
+
* 面板请求。
|
|
47
|
+
*
|
|
48
|
+
* 全局 CLI 顺带更新(方案 B:desktop profile 与全局 npm 安装是两份独立拷贝,
|
|
49
|
+
* 只更 profile 会版本偏斜):profile 阶段落定(installed 装完 / up-to-date
|
|
50
|
+
* 短路)后跑独立的全局阶段,目标是两份安装收敛到同一精确版本:
|
|
51
|
+
* - detectGlobalCli 零 spawn 纯文件系统检测:PATH 各目录 + /opt/homebrew/bin
|
|
52
|
+
* + /usr/local/bin 找 `maoying`(win32 `maoying.cmd`),existsSync 预筛;
|
|
53
|
+
* 命中后 realpathSync 该 bin 两级上溯得包根(bin/maoying.mjs → 包根),
|
|
54
|
+
* 包根 package.json 的 name 必须是 dsh-maoying 才认(防怪链接);包根
|
|
55
|
+
* realpath 落在本 profile 目录内(如 profile 自带 .bin/maoying)→ 不是
|
|
56
|
+
* 全局安装,跳过该候选继续找。找不到任何候选 → { found: false }。
|
|
57
|
+
* - 找不到全局安装 → globalCli: { ok: true, skipped: 'missing' },不 spawn;
|
|
58
|
+
* - 全局 version 与精确目标同版 → skipped: 'same',不 spawn;否则 spawn
|
|
59
|
+
* `npm i -g dsh-maoying@<target>`(cwd=os.tmpdir()、独立 120s 超时,npm
|
|
60
|
+
* 候选链与 pnpm 同款 spawn ENOENT 探测:裸 npm → execPath 同目录 →
|
|
61
|
+
* /opt/homebrew/bin → /usr/local/bin → /opt/homebrew/opt/node@XX/bin)。
|
|
62
|
+
* - 全局阶段失败(npm 缺失 / 非零 / 超时)**不拖累整体结果**:profile 已装
|
|
63
|
+
* 好,重启照常生效;globalCli.ok:false + 中文 error(含手动出路
|
|
64
|
+
* `npm i -g dsh-maoying@<target>`)如实透出。dev-link / unsupported-layout /
|
|
65
|
+
* no-target / profile-missing 等失败路径维持整单拒绝,不碰全局。
|
|
19
66
|
*
|
|
20
67
|
* 所有失败路径都不抛错,一律收敛为 { ok: false, kind, message } 结果对象;
|
|
21
|
-
* 成功返回 { ok: true, kind: 'installed', from, to, restartRequired: true
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
68
|
+
* 成功返回 { ok: true, kind: 'installed', from, to, restartRequired: true,
|
|
69
|
+
* globalCli },已是最新短路返回 { ok: true, kind: 'up-to-date', from,
|
|
70
|
+
* globalCli }(globalCli 形状见上节)。硬约束:装完后运行
|
|
71
|
+
* 中的进程仍是旧代码,必须重启 DeepSeek 才生效——restartRequired 恒 true,UI
|
|
72
|
+
* 文案必须明说,不得暗示装完即生效。依赖全部可注入(spawnImpl / realpathImpl /
|
|
73
|
+
* existsImpl / readdirImpl / fetchImpl / platform …),对齐 lib/version.mjs 的
|
|
74
|
+
* fetchImpl 模式,方便测试。
|
|
26
75
|
*/
|
|
27
76
|
import { spawn as nodeSpawn } from 'node:child_process'
|
|
28
|
-
import {
|
|
29
|
-
import { readFile } from 'node:fs/promises'
|
|
77
|
+
import { existsSync, readFileSync, readdirSync, realpathSync } from 'node:fs'
|
|
30
78
|
import os from 'node:os'
|
|
31
79
|
import path from 'node:path'
|
|
32
80
|
import { fileURLToPath } from 'node:url'
|
|
33
81
|
|
|
34
|
-
import {
|
|
82
|
+
import {
|
|
83
|
+
compareVersions,
|
|
84
|
+
currentVersion,
|
|
85
|
+
fetchLatestFromMirror,
|
|
86
|
+
fetchLatestFromRegistry,
|
|
87
|
+
invalidateVersionCache,
|
|
88
|
+
} from './version.mjs'
|
|
35
89
|
|
|
36
90
|
/** 安装超时:pnpm 冷缓存 + 国内网络最坏留足 3 分钟。 */
|
|
37
91
|
export const SELF_UPDATE_TIMEOUT_MS = 180 * 1000
|
|
38
|
-
/**
|
|
39
|
-
const
|
|
92
|
+
/** 全局 CLI 更新超时:npm i -g 单包安装,独立于 profile 的 180s,2 分钟收敛。 */
|
|
93
|
+
export const GLOBAL_CLI_TIMEOUT_MS = 120 * 1000
|
|
94
|
+
/** 版本预检单路(registry / mirror)超时:用户显式点了「升级」,等得起。 */
|
|
95
|
+
const VERSION_TIMEOUT_MS = 10 * 1000
|
|
96
|
+
/**
|
|
97
|
+
* 失败信息里保留的子进程输出尾部长度(stdout+stderr 合并后取尾):8KB——
|
|
98
|
+
* pnpm 的报错结论都在尾部,但镜像 404 / 对账冲突这类关键行可能被进度刷屏
|
|
99
|
+
* 顶得很深,2KB 不够排障。
|
|
100
|
+
*/
|
|
101
|
+
const OUTPUT_TAIL_BYTES = 8 * 1024
|
|
102
|
+
/** 输出累积硬上限:超过先截一版,防异常刷爆内存。 */
|
|
103
|
+
const OUTPUT_HARD_CAP_BYTES = 1024 * 1024
|
|
104
|
+
/** SIGTERM 后留给进程自行退出的宽限,超时升级 SIGKILL。 */
|
|
105
|
+
const TERM_GRACE_MS = 5 * 1000
|
|
40
106
|
|
|
41
107
|
/**
|
|
42
108
|
* 插件包自身目录(package.json 所在目录)。抽成函数便于测试注入假安装位。
|
|
@@ -46,34 +112,57 @@ function defaultPkgDir() {
|
|
|
46
112
|
}
|
|
47
113
|
|
|
48
114
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
115
|
+
* 缺省子进程执行器(可被 spawnImpl 注入替换,测试用):spawn 任意命令,合并
|
|
116
|
+
* 收集 stdout+stderr,永不 reject,统一收敛为
|
|
117
|
+
* { code, output, error?, errorCode? }
|
|
118
|
+
* error 仅子进程异常(spawn 失败 / 超时)时存在;errorCode 为结构化失败码:
|
|
119
|
+
* spawn 失败透传 err.code(如 'ENOENT',候选链据此换下一路),超时收敛为
|
|
120
|
+
* 'ETIMEDOUT'。timeoutLabel 只用于超时文案标明哪条通道超时。超时先 SIGTERM,
|
|
121
|
+
* TERM_GRACE_MS 后仍不退则 SIGKILL 兜底(pnpm 尊重 SIGTERM;SIGKILL 保证
|
|
122
|
+
* close 事件必然到达,调用方绝不悬挂)。
|
|
51
123
|
*/
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'/opt/homebrew/bin/pnpm',
|
|
56
|
-
'/usr/local/bin/pnpm',
|
|
57
|
-
path.join(home, '.local', 'share', 'pnpm', 'pnpm'),
|
|
58
|
-
path.join(home, 'Library', 'pnpm', 'pnpm'),
|
|
59
|
-
]
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** 解析 pnpm 可执行文件:PATH 各目录在前、常见安装位兜底,去重后逐个可执行性检查。 */
|
|
63
|
-
function resolvePnpm(env, { existsImpl, accessImpl }) {
|
|
64
|
-
const pathEnv = typeof env.PATH === 'string' ? env.PATH : ''
|
|
65
|
-
const fromPath = pathEnv.split(path.delimiter).filter((d) => d !== '').map((d) => path.join(d, 'pnpm'))
|
|
66
|
-
const candidates = [...new Set([...fromPath, ...extraPnpmCandidates(env)])]
|
|
67
|
-
for (const candidate of candidates) {
|
|
124
|
+
export function defaultSpawn(command, args, options, timeoutMs, timeoutLabel) {
|
|
125
|
+
return new Promise((resolve) => {
|
|
126
|
+
let child
|
|
68
127
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
128
|
+
child = nodeSpawn(command, args, options)
|
|
129
|
+
} catch (err) {
|
|
130
|
+
// 参数非法等同步异常:与 error 事件同一收敛形状。
|
|
131
|
+
resolve({ code: null, output: '', error: String(err?.message ?? err), errorCode: err?.code ?? null })
|
|
132
|
+
return
|
|
74
133
|
}
|
|
75
|
-
|
|
76
|
-
|
|
134
|
+
let output = ''
|
|
135
|
+
const onChunk = (chunk) => {
|
|
136
|
+
output += chunk.toString()
|
|
137
|
+
if (output.length > OUTPUT_HARD_CAP_BYTES) output = output.slice(-OUTPUT_TAIL_BYTES)
|
|
138
|
+
}
|
|
139
|
+
child.stdout?.on('data', onChunk)
|
|
140
|
+
child.stderr?.on('data', onChunk)
|
|
141
|
+
let timedOut = false
|
|
142
|
+
const timer = setTimeout(() => {
|
|
143
|
+
timedOut = true
|
|
144
|
+
try { child.kill('SIGTERM') } catch { /* 已退出:忽略 */ }
|
|
145
|
+
}, timeoutMs)
|
|
146
|
+
const killTimer = setTimeout(() => {
|
|
147
|
+
if (!timedOut) return
|
|
148
|
+
try { child.kill('SIGKILL') } catch { /* 已退出:忽略 */ }
|
|
149
|
+
}, timeoutMs + TERM_GRACE_MS)
|
|
150
|
+
const settle = (r) => { clearTimeout(timer); clearTimeout(killTimer); resolve(r) }
|
|
151
|
+
child.on('error', (err) => {
|
|
152
|
+
settle({ code: null, output: tailOutput(output), error: String(err?.message ?? err), errorCode: err?.code ?? null })
|
|
153
|
+
})
|
|
154
|
+
child.on('close', (code) => {
|
|
155
|
+
settle(timedOut
|
|
156
|
+
? { code, output: tailOutput(output), error: `${timeoutLabel} 超时(${Math.round(timeoutMs / 1000)} 秒)已中止`, errorCode: 'ETIMEDOUT' }
|
|
157
|
+
: { code, output: tailOutput(output) })
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** 取尾部:超长只留最后 OUTPUT_TAIL_BYTES。 */
|
|
163
|
+
function tailOutput(text) {
|
|
164
|
+
const s = String(text ?? '')
|
|
165
|
+
return s.length > OUTPUT_TAIL_BYTES ? s.slice(-OUTPUT_TAIL_BYTES) : s
|
|
77
166
|
}
|
|
78
167
|
|
|
79
168
|
/**
|
|
@@ -98,48 +187,213 @@ function dshHomeFromEnv(env) {
|
|
|
98
187
|
return raw
|
|
99
188
|
}
|
|
100
189
|
|
|
190
|
+
/** env 里的用户 home(PATH 候选拼接用;缺失回退 os.homedir())。 */
|
|
191
|
+
function homeFromEnv(env) {
|
|
192
|
+
const raw = typeof env.HOME === 'string' && env.HOME.trim() !== '' ? env.HOME.trim() : ''
|
|
193
|
+
return raw !== '' ? raw : os.homedir()
|
|
194
|
+
}
|
|
195
|
+
|
|
101
196
|
/**
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
197
|
+
* 组装 pnpm 候选链(详见文件头注):裸 pnpm → 裸 corepack(+['pnpm'] 前缀)
|
|
198
|
+
* → 各绝对路径落位(existsSync 预筛,不存在不浪费 spawn;去重保序)。
|
|
199
|
+
* win32 的 pnpm / corepack 是 .cmd shim,命令补 .cmd 后缀(配合 shell:true);
|
|
200
|
+
* darwin/linux 补空串。/opt/homebrew 系仅 darwin+homebrew 存在,existsSync
|
|
201
|
+
* 自然过滤,无需按平台分支。
|
|
105
202
|
*/
|
|
106
|
-
|
|
203
|
+
function buildCandidates({ platform, env, execPath, existsImpl, readdirImpl }) {
|
|
204
|
+
const suffix = platform === 'win32' ? '.cmd' : ''
|
|
205
|
+
const candidates = [
|
|
206
|
+
{ command: `pnpm${suffix}`, argsPrefix: [], label: 'pnpm' },
|
|
207
|
+
{ command: `corepack${suffix}`, argsPrefix: ['pnpm'], label: 'corepack pnpm' },
|
|
208
|
+
]
|
|
209
|
+
const absolute = ['/opt/homebrew/bin/corepack']
|
|
107
210
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
211
|
+
// /opt/homebrew/opt/<node 版本目录>/bin/corepack:目录不存在(非 ARM
|
|
212
|
+
// homebrew mac)readdir 抛错即跳过;命中多个 node 版本时按名排序全收。
|
|
213
|
+
const nodeDirs = readdirImpl('/opt/homebrew/opt')
|
|
214
|
+
.filter((name) => name.startsWith('node'))
|
|
215
|
+
.sort()
|
|
216
|
+
for (const dir of nodeDirs) absolute.push(path.join('/opt/homebrew/opt', dir, 'bin', `corepack${suffix}`))
|
|
217
|
+
} catch { /* 无 /opt/homebrew/opt:跳过这组候选 */ }
|
|
218
|
+
absolute.push(path.join(path.dirname(execPath), `corepack${suffix}`))
|
|
219
|
+
const home = homeFromEnv(env)
|
|
220
|
+
absolute.push(path.join(home, 'Library', 'pnpm', `pnpm${suffix}`))
|
|
221
|
+
absolute.push(path.join(home, '.local', 'share', 'pnpm', `pnpm${suffix}`))
|
|
222
|
+
for (const p of [...new Set(absolute)]) {
|
|
223
|
+
if (existsImpl(p)) candidates.push({ command: p, argsPrefix: [], label: p })
|
|
224
|
+
}
|
|
225
|
+
return candidates
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 全局 maoying CLI 检测(零 spawn,纯文件系统;机制见文件头注「全局 CLI 顺带
|
|
230
|
+
* 更新」节)。bin 候选位 = env.PATH 各目录 + /opt/homebrew/bin + /usr/local/bin
|
|
231
|
+
* (去重保序),找 `maoying`(win32 `maoying.cmd`),existsSync 预筛;命中后
|
|
232
|
+
* realpathSync 两级上溯得包根,包根 package.json 的 name 必须是 dsh-maoying
|
|
233
|
+
* 才认;包根 realpath 落在 profileDir 内(或就是它)→ 是 profile 自己的
|
|
234
|
+
* .bin/maoying,跳过该候选继续找。全落空 → { found: false }。
|
|
235
|
+
* 任何一步异常(断链 symlink / 包根不可读 / JSON 坏)都只跳过该候选,不抛错。
|
|
236
|
+
*/
|
|
237
|
+
function detectGlobalCli({ env, platform, profileDir, existsImpl, realpathImpl, readImpl }) {
|
|
238
|
+
const binName = `maoying${platform === 'win32' ? '.cmd' : ''}`
|
|
239
|
+
const dirs = String(typeof env.PATH === 'string' ? env.PATH : '')
|
|
240
|
+
.split(path.delimiter)
|
|
241
|
+
.map((d) => d.trim())
|
|
242
|
+
.filter((d) => d !== '')
|
|
243
|
+
dirs.push('/opt/homebrew/bin', '/usr/local/bin')
|
|
244
|
+
// profile 目录 realpath 只算一次(目录此刻必然存在——package.json 就绪闸已过;
|
|
245
|
+
// 读不到真路径就退回原路径,比对退化为保守不误杀全局候选)。
|
|
246
|
+
let realProfileDir = null
|
|
247
|
+
try { realProfileDir = path.resolve(realpathImpl(profileDir)) } catch { realProfileDir = path.resolve(profileDir) }
|
|
248
|
+
for (const dir of [...new Set(dirs)]) {
|
|
249
|
+
const bin = path.join(dir, binName)
|
|
250
|
+
if (!existsImpl(bin)) continue
|
|
251
|
+
let cli
|
|
252
|
+
try { cli = path.resolve(realpathImpl(bin)) } catch { continue }
|
|
253
|
+
const root = path.dirname(path.dirname(cli))
|
|
254
|
+
let manifest
|
|
255
|
+
try { manifest = JSON.parse(readImpl(path.join(root, 'package.json'))) } catch { continue }
|
|
256
|
+
if (manifest?.name !== 'dsh-maoying') continue
|
|
257
|
+
let realRoot
|
|
258
|
+
try { realRoot = path.resolve(realpathImpl(root)) } catch { realRoot = path.resolve(root) }
|
|
259
|
+
if (realRoot === realProfileDir || realRoot.startsWith(realProfileDir + path.sep)) continue
|
|
260
|
+
return {
|
|
261
|
+
found: true,
|
|
262
|
+
bin,
|
|
263
|
+
root,
|
|
264
|
+
version: typeof manifest.version === 'string' ? manifest.version : '',
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return { found: false }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* 组装 npm 候选链(全局 CLI 更新用,与 pnpm 链同款 spawn ENOENT 探测语义):
|
|
272
|
+
* 裸 npm → execPath 同目录(node 与 npm 同落位的常规发行形态)→
|
|
273
|
+
* /opt/homebrew/bin → /usr/local/bin → /opt/homebrew/opt/node@XX/bin(readdirSync
|
|
274
|
+
* 过滤 node 前缀目录,兜 keg-only node@XX)。绝对路径候选 existsSync 预筛;
|
|
275
|
+
* 去重保序。win32 的 npm 是 .cmd shim,命令补后缀(配合 shell:true)。
|
|
276
|
+
*/
|
|
277
|
+
function buildNpmCandidates({ platform, execPath, existsImpl, readdirImpl }) {
|
|
278
|
+
const suffix = platform === 'win32' ? '.cmd' : ''
|
|
279
|
+
const candidates = [{ command: `npm${suffix}`, label: 'npm' }]
|
|
280
|
+
const absolute = [
|
|
281
|
+
path.join(path.dirname(execPath), `npm${suffix}`),
|
|
282
|
+
'/opt/homebrew/bin/npm',
|
|
283
|
+
'/usr/local/bin/npm',
|
|
284
|
+
]
|
|
285
|
+
try {
|
|
286
|
+
const nodeDirs = readdirImpl('/opt/homebrew/opt')
|
|
287
|
+
.filter((name) => name.startsWith('node'))
|
|
288
|
+
.sort()
|
|
289
|
+
for (const dir of nodeDirs) absolute.push(path.join('/opt/homebrew/opt', dir, 'bin', `npm${suffix}`))
|
|
290
|
+
} catch { /* 无 /opt/homebrew/opt:跳过这组候选 */ }
|
|
291
|
+
for (const p of [...new Set(absolute)]) {
|
|
292
|
+
if (existsImpl(p)) candidates.push({ command: p, label: p })
|
|
293
|
+
}
|
|
294
|
+
return candidates
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* 全局 CLI 阶段(profile 阶段落定后跑;失败绝不拖累整体结果,形状见文件头注)。
|
|
299
|
+
* 返回 { ok: true, skipped: 'missing' } / { ok: true, skipped: 'same', version } /
|
|
300
|
+
* { ok: true, from, to: targetVersion } / { ok: false, error, output? }。
|
|
301
|
+
* spawn 走 npm 候选链(仅 ENOENT 换下一路),cwd=os.tmpdir()(npm 全局装与
|
|
302
|
+
* cwd 无关,不该在 profile / 仓库里跑),独立 globalTimeoutMs 超时。
|
|
303
|
+
*/
|
|
304
|
+
async function updateGlobalCliStage({ targetVersion, profileDir }, deps) {
|
|
305
|
+
const { env, platform, execPath, spawnImpl, existsImpl, readdirImpl, realpathImpl, readImpl, logger, globalTimeoutMs } = deps
|
|
306
|
+
const g = detectGlobalCli({ env, platform, profileDir, existsImpl, realpathImpl, readImpl })
|
|
307
|
+
if (!g.found) return { ok: true, skipped: 'missing' }
|
|
308
|
+
// 版本比对('' = 包根 package.json 缺 version,当旧版处理装到精确目标)。
|
|
309
|
+
if (g.version !== '' && compareVersions(g.version, targetVersion) === 0) {
|
|
310
|
+
logger?.info?.(`maoying 创作台: 全局 maoying CLI 已是 ${g.version},跳过更新`)
|
|
311
|
+
return { ok: true, skipped: 'same', version: g.version }
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const spawnOptions = {
|
|
315
|
+
cwd: os.tmpdir(),
|
|
316
|
+
env,
|
|
317
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
318
|
+
shell: platform === 'win32',
|
|
319
|
+
}
|
|
320
|
+
const candidates = buildNpmCandidates({ platform, execPath, existsImpl, readdirImpl })
|
|
321
|
+
const installArgs = ['i', '-g', `dsh-maoying@${targetVersion}`]
|
|
322
|
+
const manual = `npm i -g dsh-maoying@${targetVersion}`
|
|
323
|
+
|
|
324
|
+
let proc = null
|
|
325
|
+
for (const cand of candidates) {
|
|
326
|
+
proc = await spawnImpl(cand.command, installArgs, spawnOptions, globalTimeoutMs, 'npm i -g')
|
|
327
|
+
if (proc?.errorCode !== 'ENOENT') break
|
|
328
|
+
}
|
|
329
|
+
const output = tailOutput(proc?.output)
|
|
330
|
+
|
|
331
|
+
// 以下失败路径一律 globalCli.ok:false + 中文 error 含手动出路;整体结果由
|
|
332
|
+
// 调用方维持 ok:true(profile 已装好,全局失败不追责整单)。
|
|
333
|
+
if (proc?.errorCode === 'ENOENT') {
|
|
334
|
+
const tried = candidates.map((c) => c.label).join(' / ')
|
|
335
|
+
const error = `未找到可用的 npm(已试 ${tried}),全局 CLI 未更新;可手动执行:${manual}`
|
|
336
|
+
logger?.warn?.(`maoying 创作台: 全局 CLI 更新失败(npm-missing): ${error}`)
|
|
337
|
+
return { ok: false, error }
|
|
338
|
+
}
|
|
339
|
+
if (proc?.errorCode === 'ETIMEDOUT') {
|
|
340
|
+
logger?.warn?.('maoying 创作台: 全局 CLI 更新失败(timeout): ' + String(proc?.error ?? ''))
|
|
341
|
+
return {
|
|
342
|
+
ok: false,
|
|
343
|
+
error: `npm i -g 超时(${Math.round(globalTimeoutMs / 1000)} 秒)已中止,全局 CLI 未更新;可手动执行:${manual}`,
|
|
344
|
+
output,
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (proc?.error) {
|
|
348
|
+
const error = `npm 启动失败: ${proc.error},全局 CLI 未更新;可手动执行:${manual}`
|
|
349
|
+
logger?.warn?.(`maoying 创作台: 全局 CLI 更新失败(spawn): ${proc.error}`)
|
|
350
|
+
return { ok: false, error, output }
|
|
113
351
|
}
|
|
352
|
+
if (proc?.code !== 0) {
|
|
353
|
+
const error = `npm i -g 失败(exit ${proc?.code}),全局 CLI 未更新;可手动执行:${manual}`
|
|
354
|
+
logger?.warn?.(`maoying 创作台: 全局 CLI 更新失败(exit ${proc?.code}): ${output}`)
|
|
355
|
+
return { ok: false, error, output }
|
|
356
|
+
}
|
|
357
|
+
logger?.info?.(`maoying 创作台: 全局 maoying CLI ${g.version} → ${targetVersion}`)
|
|
358
|
+
return { ok: true, from: g.version, to: targetVersion }
|
|
114
359
|
}
|
|
115
360
|
|
|
116
361
|
/** 组装最小注入面:缺省走真实 Node 实现,测试逐项替换。 */
|
|
117
|
-
function normalizeDeps(opts) {
|
|
118
|
-
const env = opts.env ?? process.env
|
|
362
|
+
function normalizeDeps(opts = {}) {
|
|
119
363
|
return {
|
|
120
|
-
env,
|
|
121
|
-
|
|
364
|
+
env: opts.env ?? process.env,
|
|
365
|
+
platform: opts.platform ?? process.platform,
|
|
366
|
+
execPath: opts.execPath ?? process.execPath,
|
|
367
|
+
spawnImpl: opts.spawnImpl ?? defaultSpawn,
|
|
122
368
|
pkgDirImpl: opts.pkgDirImpl ?? defaultPkgDir,
|
|
123
369
|
realpathImpl: opts.realpathImpl ?? ((p) => realpathSync(p)),
|
|
124
370
|
existsImpl: opts.existsImpl ?? ((p) => existsSync(p)),
|
|
125
|
-
|
|
126
|
-
readImpl:
|
|
127
|
-
|
|
128
|
-
((p) => readFile(p, 'utf8')),
|
|
371
|
+
readdirImpl: opts.readdirImpl ?? ((p) => readdirSync(p)),
|
|
372
|
+
readImpl: opts.readImpl ?? ((p) => readFileSync(p, 'utf8')),
|
|
373
|
+
fetchImpl: opts.fetchImpl ?? null,
|
|
129
374
|
logger: opts.logger ?? console,
|
|
130
375
|
timeoutMs:
|
|
131
376
|
typeof opts.timeoutMs === 'number' && opts.timeoutMs > 0 ? opts.timeoutMs : SELF_UPDATE_TIMEOUT_MS,
|
|
377
|
+
globalTimeoutMs:
|
|
378
|
+
typeof opts.globalTimeoutMs === 'number' && opts.globalTimeoutMs > 0
|
|
379
|
+
? opts.globalTimeoutMs
|
|
380
|
+
: GLOBAL_CLI_TIMEOUT_MS,
|
|
132
381
|
}
|
|
133
382
|
}
|
|
134
383
|
|
|
135
384
|
/**
|
|
136
385
|
* selfUpdate 主体(永不 reject;concurrency 去重与兜底 try/catch 在外层)。
|
|
137
386
|
* @returns {Promise<{ok: boolean, kind: string, from?: string|null, to?: string|null,
|
|
138
|
-
* restartRequired?: boolean, message?: string
|
|
387
|
+
* restartRequired?: boolean, message?: string,
|
|
388
|
+
* globalCli?: {ok: true, skipped: 'missing'} | {ok: true, skipped: 'same', version: string} |
|
|
389
|
+
* {ok: true, from: string, to: string} | {ok: false, error: string, output?: string}}>}
|
|
390
|
+
* globalCli 仅两个成功路径携带,形状见文件头注「全局 CLI 顺带更新」节。
|
|
139
391
|
*/
|
|
140
392
|
async function runSelfUpdate(opts = {}) {
|
|
141
|
-
const {
|
|
142
|
-
|
|
393
|
+
const {
|
|
394
|
+
env, platform, execPath, spawnImpl, pkgDirImpl, realpathImpl, existsImpl, readdirImpl, readImpl,
|
|
395
|
+
fetchImpl, logger, timeoutMs, globalTimeoutMs,
|
|
396
|
+
} = normalizeDeps(opts)
|
|
143
397
|
|
|
144
398
|
// ── a) 安装形态判定 ──────────────────────────────────────────────────────
|
|
145
399
|
const pkgDir = pkgDirImpl()
|
|
@@ -169,75 +423,106 @@ async function runSelfUpdate(opts = {}) {
|
|
|
169
423
|
}
|
|
170
424
|
}
|
|
171
425
|
|
|
172
|
-
// ── b)
|
|
173
|
-
|
|
174
|
-
|
|
426
|
+
// ── b) 版本预检:拿不到 latest 绝不做半截升级(宁可不动现有安装)─────────
|
|
427
|
+
// 官方源失败再试 npmmirror 镜像(点「升级」的用户网络本来就在出状况,
|
|
428
|
+
// 多一路快路实打实救回一次升级);仍拿不到如实报 no-target。
|
|
429
|
+
const { version: current } = await currentVersion()
|
|
430
|
+
const reg = await fetchLatestFromRegistry({ fetchImpl, timeoutMs: VERSION_TIMEOUT_MS })
|
|
431
|
+
const mirror = reg.ok ? null : await fetchLatestFromMirror({ fetchImpl, timeoutMs: VERSION_TIMEOUT_MS })
|
|
432
|
+
const manifest = reg.ok ? reg : mirror
|
|
433
|
+
if (!manifest.ok) {
|
|
175
434
|
return {
|
|
176
435
|
ok: false,
|
|
177
|
-
kind: '
|
|
178
|
-
message:
|
|
436
|
+
kind: 'no-target',
|
|
437
|
+
message: `无法确定最新版本:官方源 ${reg.error};镜像 ${manifest.error}`,
|
|
179
438
|
}
|
|
180
439
|
}
|
|
440
|
+
const target = manifest.latest // 形如 v0.2.3
|
|
441
|
+
const targetVersion = target.replace(/^v/, '')
|
|
442
|
+
// 全局 CLI 阶段的依赖包(两个成功路径各跑一次;失败路径整单拒绝、碰都不碰)。
|
|
443
|
+
const globalDeps = { env, platform, execPath, spawnImpl, existsImpl, readdirImpl, realpathImpl, readImpl, logger, globalTimeoutMs }
|
|
444
|
+
if (compareVersions(target, current) <= 0) {
|
|
445
|
+
logger?.info?.(`maoying 创作台: self-update 短路:本地 ${current} 已不落后线上 ${target}`)
|
|
446
|
+
// 方案 B 语义 = 两份同版:profile 已是 target 时全局 CLI 若仍落后,照样更到
|
|
447
|
+
// 同一精确版本(全局没装 / 已同版时阶段内自行跳过,不 spawn)。
|
|
448
|
+
const globalCli = await updateGlobalCliStage({ targetVersion, profileDir }, globalDeps)
|
|
449
|
+
return { ok: true, kind: 'up-to-date', from: current, globalCli }
|
|
450
|
+
}
|
|
181
451
|
|
|
182
|
-
|
|
183
|
-
|
|
452
|
+
// ── c) profile 就绪判定:目录没有 package.json 说明未初始化,诚实失败,
|
|
453
|
+
// 不硬跑(pnpm 会在空目录另起一份 package.json,装到错误的位置)。
|
|
454
|
+
if (!existsImpl(path.join(profileDir, 'package.json'))) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
kind: 'profile-missing',
|
|
458
|
+
message: `profile 未初始化(${profileDir} 缺 package.json),无法就地升级,请先重启 DeepSeek 或重新安装插件`,
|
|
459
|
+
}
|
|
460
|
+
}
|
|
184
461
|
|
|
185
|
-
// ──
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
462
|
+
// ── d) pnpm 候选链:spawn ENOENT(命令不存在)才换下一路,EACCES / 超时 /
|
|
463
|
+
// 非零退出都是真实运行结果,如实返回不换通道。cwd 就是 profile 目录
|
|
464
|
+
// (pnpm 的操作对象是它),不带 --dir;装精确版本而非 @latest(避开
|
|
465
|
+
// pnpm 新鲜度防护 / 镜像 packument 滞后的静默空转)。
|
|
466
|
+
// win32 的 pnpm/corepack 是 .cmd shim,必须借 shell 拉起(Node 直接
|
|
467
|
+
// spawn .cmd 会 EINVAL,见 CVE-2024-27980 加固),参数保持数组形式。
|
|
468
|
+
const useShell = platform === 'win32'
|
|
469
|
+
const spawnOptions = {
|
|
470
|
+
cwd: profileDir,
|
|
471
|
+
env,
|
|
472
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
473
|
+
shell: useShell,
|
|
474
|
+
}
|
|
475
|
+
const candidates = buildCandidates({ platform, env, execPath, existsImpl, readdirImpl })
|
|
476
|
+
const installArgs = ['add', `dsh-maoying@${targetVersion}`]
|
|
477
|
+
|
|
478
|
+
let proc = null
|
|
479
|
+
for (const cand of candidates) {
|
|
480
|
+
proc = await spawnImpl(cand.command, [...cand.argsPrefix, ...installArgs], spawnOptions, timeoutMs, 'pnpm add')
|
|
481
|
+
if (proc?.errorCode !== 'ENOENT') break
|
|
482
|
+
}
|
|
483
|
+
const output = tailOutput(proc?.output)
|
|
484
|
+
|
|
485
|
+
// 全候选 ENOENT:给可操作的出路,不裸抛 spawn pnpm ENOENT(本修复的病灶)。
|
|
486
|
+
if (proc?.errorCode === 'ENOENT') {
|
|
487
|
+
const tried = candidates.map((c) => c.label).join(' / ')
|
|
488
|
+
return {
|
|
489
|
+
ok: false,
|
|
490
|
+
kind: 'pnpm-missing',
|
|
491
|
+
message: `未找到可用的 pnpm(已试 ${tried})。可先在终端执行 corepack enable 并重启 DSH 后重试;或手动升级:cd ${profileDir} && corepack pnpm add dsh-maoying@${targetVersion}`,
|
|
195
492
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
}
|
|
216
|
-
const tailText = `${out.toString('utf8')}\n${errBuf.toString('utf8')}`.trim()
|
|
217
|
-
resolve({
|
|
218
|
-
failed: true,
|
|
219
|
-
kind: 'pnpm-failed',
|
|
220
|
-
message: `pnpm add 失败(${signal != null ? `signal ${signal}` : `exit ${code}`}):${tailText !== '' ? tailText : '无输出'}`,
|
|
221
|
-
})
|
|
493
|
+
}
|
|
494
|
+
if (proc?.errorCode === 'ETIMEDOUT') {
|
|
495
|
+
// 超时:SIGTERM 已发(宽限后 SIGKILL),如实收敛不换通道。
|
|
496
|
+
logger?.warn?.('maoying 创作台: self-update 失败(timeout): ' + String(proc?.error ?? ''))
|
|
497
|
+
return {
|
|
498
|
+
ok: false,
|
|
499
|
+
kind: 'timeout',
|
|
500
|
+
message: `pnpm 安装超时(${Math.round(timeoutMs / 1000)} 秒)已中止,请检查网络后重试`,
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (proc?.error) {
|
|
504
|
+
// 其他子进程异常(EACCES 等真实结果,不换通道、不猜原因)。
|
|
505
|
+
logger?.warn?.(`maoying 创作台: self-update 失败(spawn): ${proc.error}`)
|
|
506
|
+
return { ok: false, kind: 'nonzero', message: `pnpm 启动失败: ${proc.error}` }
|
|
507
|
+
}
|
|
508
|
+
if (proc?.code !== 0) {
|
|
509
|
+
return {
|
|
510
|
+
ok: false,
|
|
511
|
+
kind: 'nonzero',
|
|
512
|
+
message: `pnpm add 失败(exit ${proc?.code}):${output !== '' ? output : '无输出'}`,
|
|
222
513
|
}
|
|
223
|
-
child.on?.('exit', onExit)
|
|
224
|
-
child.on?.('error', (err) => {
|
|
225
|
-
clearTimeout(timer)
|
|
226
|
-
resolve({ failed: true, kind: 'spawn-error', message: `pnpm 启动失败: ${err?.message ?? err}` })
|
|
227
|
-
})
|
|
228
|
-
})
|
|
229
|
-
if (exitInfo.failed) {
|
|
230
|
-
logger?.warn?.(`maoying 创作台: self-update 失败(${exitInfo.kind}): ${exitInfo.message}`)
|
|
231
|
-
return { ok: false, kind: exitInfo.kind, message: exitInfo.message }
|
|
232
514
|
}
|
|
233
515
|
|
|
234
|
-
// ──
|
|
235
|
-
|
|
236
|
-
//
|
|
237
|
-
//
|
|
516
|
+
// ── e) 成功:版本缓存立刻失效(装的是磁盘新包,进程内 1h 缓存已是旧结论)。
|
|
517
|
+
// to 取预检到的精确目标版本(退出码即成败信号,不做磁盘后验——本进程
|
|
518
|
+
// 代码不随升级变化,无从对账)。随后跑全局 CLI 阶段(方案 B:desktop
|
|
519
|
+
// profile 与全局 npm 安装是两份独立拷贝,顺带更到同一精确版本);全局
|
|
520
|
+
// 阶段失败不拖累整体——profile 已装好,重启照常生效,globalCli.ok:false
|
|
521
|
+
// 如实透出。
|
|
238
522
|
invalidateVersionCache()
|
|
239
|
-
logger?.info?.(`maoying 创作台: self-update 完成 ${
|
|
240
|
-
|
|
523
|
+
logger?.info?.(`maoying 创作台: self-update 完成 ${current} → ${targetVersion}(重启 DeepSeek 后生效)`)
|
|
524
|
+
const globalCli = await updateGlobalCliStage({ targetVersion, profileDir }, globalDeps)
|
|
525
|
+
return { ok: true, kind: 'installed', from: current, to: targetVersion, restartRequired: true, globalCli }
|
|
241
526
|
}
|
|
242
527
|
|
|
243
528
|
// in-flight 去重:并发的 selfUpdate(连点按钮 / 双面板)复用同一次安装
|
package/package.json
CHANGED
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
|
|
51
51
|
## 开工纪律
|
|
52
52
|
|
|
53
|
-
1. 每次开工先读 novel/project.yml
|
|
53
|
+
1. 每次开工先读 novel/project.yml(书名/类型/篇幅/阶段)、novel/index.yml(章节概览)与 novel/style.md(写作风格基准)掌握全局;style.md 缺失时新书走冷启动①对齐补建,旧项目提示用户是否补建;项目不存在时先走「新书冷启动」(见 Collab 四步)。
|
|
54
54
|
2. 不确定文件格式时读 novel/README.md(数据规范速查)。
|
|
55
55
|
3. 上下文按需取,不全量读:写到哪章,只读该章 frontmatter(大纲+五维字段)、前一章结尾约 1500 字(衔接)、出场角色卡(novel/characters/)、活跃伏笔(novel/foreshadows.yml 中 status 为 planted/progressing 的条目)。
|
|
56
56
|
4. 委派时把必要上下文写进任务描述(章节号、大纲要点、涉及角色),别让子代理自己大海捞针。
|
|
@@ -65,14 +65,14 @@
|
|
|
65
65
|
① 衔接检查:读前一章结尾,确认本章开头不断裂、不重复;
|
|
66
66
|
② 委派 outline 出/改本章大纲(含五维字段:cognitive_frame 认知框架 / emotional_anchor 情感锚点 / world_texture 世界质感 / suspense 具体悬念 / interpersonal_tension 人际张力),你过目认可才进下一步;
|
|
67
67
|
③ 委派 write 写正文(status 置 writing):任务里给全大纲、前章结尾要点、出场角色、需推进的伏笔;
|
|
68
|
-
④ 委派 review 审稿(status 置 review
|
|
68
|
+
④ 委派 review 审稿(status 置 review):审稿清单(语言与节奏/人设符合/不剧透后期/伏笔推进合理/章尾钩子/风格符合)+ 四维一致性扫描(冲突/时间线/伏笔/设定);
|
|
69
69
|
⑤ 审稿有问题:continuable 角色就地续修(write 用 send_message 续),或你亲自微调;通过后 status 置 accepted;
|
|
70
70
|
⑥ 定稿维护:委派 lore 做该章出场记录、伏笔状态推进、时间线与记忆更新、新关系与关系变动入 relations.yml(新角色与在场者有关系必登记);index.yml 与章节现状不符时让它重建。
|
|
71
71
|
循环检查:还有非 accepted 章节 → 回 ① 继续;全部定稿 → 推进 phase。
|
|
72
72
|
|
|
73
73
|
## Collab 四步共创(新书冷启动)
|
|
74
74
|
|
|
75
|
-
①
|
|
75
|
+
① 设定共创:与用户逐项对齐最小设定集——题材、金手指(能力/代价/边界)、基调、写作风格、主角三要素(想要·阻碍·筹码)、核心反派与对抗主线、世界观底色,缺一项就追问不臆测;对齐后由你把风格基准写入 novel/style.md(格式以 novel/README.md 为准),委派 lore 落盘核心角色卡(主角、核心反派、关键配角)、世界观核心条目,并把核心角色之间的戏剧关系逐条登记进 relations.yml(a/b 用角色卡 slug、intensity 1-5、started 先记计划章,格式以 novel/README.md 为准),把设定一览简报用户,等用户明确拍板才进②;
|
|
76
76
|
② 分卷规划:委派 outline 出 volumes.yml 与各卷 summary(第一卷细化到章节号范围),简报用户,等用户明确拍板才进③;
|
|
77
77
|
③ 前三章交付:动笔前机械自检——characters/ 有主角卡与核心反派卡、world/ 有金手指与对抗格局条目、volumes.yml 在案,缺任何一项回①②补齐不硬闯;齐了才走 Writing 六步法连写前三章——黄金三章:第一章立世界钩子、第二章立主角动机、第三章立核心冲突;
|
|
78
78
|
④ 交接循环写作:切回 Writing 六步法滚动推进,之后每 5-10 章建议用户做一次 consistency 集中审查。
|
|
@@ -241,7 +241,7 @@
|
|
|
241
241
|
name: "@deepseek-ai/dsh-persona"
|
|
242
242
|
config:
|
|
243
243
|
prefix: |-
|
|
244
|
-
你是网文项目的大纲师。职责:卷章规划与章节大纲。收到委派后:先读 novel/project.yml 与 novel/
|
|
244
|
+
你是网文项目的大纲师。职责:卷章规划与章节大纲。收到委派后:先读 novel/project.yml、novel/index.yml 与 novel/style.md(若有)掌握全局,再按任务写/改 volumes.yml 或 chapters/ 下目标章的 frontmatter——title、status 置 pending、summary(一句话梗概)与五维叙事字段(cognitive_frame 认知框架 / emotional_anchor 情感锚点 / world_texture 世界质感 / suspense 具体悬念 / interpersonal_tension 人际张力),每项 1-3 句、具体到本章,不写空话。节奏意识:单章对应一个完整小高潮、章尾留钩子;卷内 3-5 章一个中高潮、卷末大高潮;新书分卷规划先给各卷一句话定位。五维字段与节拍服从 style.md 风格基调,风格与任务要求冲突时上报主编。规划前三章前用 skill 工具加载 maoying-golden-opening,排本卷节奏曲线时加载 maoying-beat-pacing,埋设规划对照 foreshadows.yml 并遵循 maoying-foreshadow-discipline。涉及新设定(新角色/新地点)只在大纲中点名,落盘归设定管理员;发现核心设定缺失(无主角卡/核心反派卡、无 volumes.yml 分卷规划)时停手上报主编缺失清单,不在大纲里自行发明设定。共创模式(project.yml mode 为 co-write / author-draft)下加一项产出:章纲 novel/outlines/vXX-cXXX.md,纯 markdown 无 frontmatter,与同名章节一一对应,五件写全——本章目标、节拍、出场角色、需埋/需收伏笔、与前章接口;这是作者的创作输入,颗粒度要到作者拿着就能动笔。完成后回报:改了哪些文件、每章一句话梗概、遗留给主编的决策点。创作方向拿不准就用 ask_user_question 问主编,选项要具体。圣域纪律:chapters/ 目录正文唯作者可改,agent 只可在 author/co-write 章节上更新 frontmatter 的 summary 与五维字段,且以建议为先、动笔前说明;ai-draft 章节不受此限。
|
|
245
245
|
On tool failure, do not retry a denied operation: report the failure compactly — what you completed, the exact error, which tool/permission you lack, and your suggested next step.
|
|
246
246
|
- id: agent-instructions
|
|
247
247
|
name: "@deepseek-ai/dsh-agent-instructions"
|
|
@@ -303,7 +303,7 @@
|
|
|
303
303
|
name: "@deepseek-ai/dsh-persona"
|
|
304
304
|
config:
|
|
305
305
|
prefix: |-
|
|
306
|
-
你是网文项目的写手。职责:按大纲写章节正文。收到委派后:读目标章 frontmatter(大纲+五维字段)、前一章结尾约 1500 字(衔接)、出场角色卡(novel/characters/)、活跃伏笔(novel/foreshadows.yml 中 planted/progressing
|
|
306
|
+
你是网文项目的写手。职责:按大纲写章节正文。收到委派后:读目标章 frontmatter(大纲+五维字段)、前一章结尾约 1500 字(衔接)、出场角色卡(novel/characters/)、活跃伏笔(novel/foreshadows.yml 中 planted/progressing 条目)、novel/style.md 写作风格基准(若有),然后把正文写入该章文件 body(frontmatter 之下),status 置 writing。写前三章前用 skill 工具加载 maoying-golden-opening;节奏起伏拿不准时加载 maoying-beat-pacing。写法纪律:正文遵守 style.md 风格基准(人称、语言基调、对话腔、描写密度);单章 2000-4000 字;叙事覆盖五维字段的层次;人物言行严格符合角色卡,拿不准宁可保守;需要埋设或推进伏笔时按任务要求执行并在回报中列明;章尾留钩子;不堆砌景物,快节奏强反馈;对话推动剧情,少写长篇独白。发现设定冲突或需要新设定:停笔,在回报中向主编上报,绝不自行改角色卡/世界观/伏笔文件(那是设定管理员的活)。status 只置 writing,不改 accepted/review(推进由主编决定)。完成后回报:字数、五维覆盖情况、伏笔异动清单、设定变更需求。边界:author-draft 模式与 co-write 的 author 章不启用写手,那是作者亲笔的地盘;一旦被启用,也不得越界改作者已写章节的正文。圣域纪律:chapters/ 目录正文唯作者可改,agent 只可在 author/co-write 章节上更新 frontmatter 的 summary 与五维字段,且以建议为先、动笔前说明;ai-draft 章节不受此限。
|
|
307
307
|
On tool failure, do not retry a denied operation: report the failure compactly — what you completed, the exact error, which tool/permission you lack, and your suggested next step.
|
|
308
308
|
- id: agent-instructions
|
|
309
309
|
name: "@deepseek-ai/dsh-agent-instructions"
|
|
@@ -367,8 +367,8 @@
|
|
|
367
367
|
name: "@deepseek-ai/dsh-persona"
|
|
368
368
|
config:
|
|
369
369
|
prefix: |-
|
|
370
|
-
你是网文项目的审校。只读不改:不修改任何文件,交付的是意见清单——逐条列问题与位置,不产出改写文本。收到委派后:先用 skill 工具加载 maoying-review-checklist
|
|
371
|
-
① 审稿清单逐项勾选:语言与节奏 / 人设符合 / 不剧透后期 / 伏笔推进合理 /
|
|
370
|
+
你是网文项目的审校。只读不改:不修改任何文件,交付的是意见清单——逐条列问题与位置,不产出改写文本。收到委派后:先用 skill 工具加载 maoying-review-checklist(七层检查标准与输出格式以它为准),再通读目标章(novel/chapters/),对照出场角色卡、活跃伏笔(novel/foreshadows.yml)、时间线(novel/timeline.yml)与前章,输出固定格式报告:
|
|
371
|
+
① 审稿清单逐项勾选:语言与节奏 / 人设符合 / 不剧透后期 / 伏笔推进合理 / 章尾钩子 / 风格符合(对照 novel/style.md,若有);
|
|
372
372
|
② 四维一致性扫描:冲突(前后矛盾)/ 时间线 / 伏笔 / 设定——每维列出具体位置(章节+段落引用)与问题描述,该维无问题也要明说「本维无问题」;
|
|
373
373
|
③ 修改建议按严重度排序(阻塞 > 应改 > 可改),建议写给主编转写手,不直接动稿。
|
|
374
374
|
④ 共创收稿(作者亲笔章):对照 novel/outlines/ 下的同名章纲评偏离——作者没照章纲写不算错,偏离如实报告,裁决归主编。
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
|
|
34
34
|
## 开工纪律
|
|
35
35
|
|
|
36
|
-
1. 每次开工先读 novel/project.yml
|
|
36
|
+
1. 每次开工先读 novel/project.yml(书名/类型/篇幅/阶段)、novel/index.yml(章节概览)与 novel/style.md(写作风格基准)掌握全局;style.md 缺失时新书走冷启动①对齐补建,旧项目提示用户是否补建;项目不存在时先走「新书冷启动」(见 Collab 四步)。
|
|
37
37
|
2. 不确定文件格式时读 novel/README.md(数据规范速查)。
|
|
38
38
|
3. 上下文按需取,不全量读:写到哪章,只读该章 frontmatter(大纲+五维字段)、前一章结尾约 1500 字(衔接)、出场角色卡(novel/characters/)、活跃伏笔(novel/foreshadows.yml 中 status 为 planted/progressing 的条目)。
|
|
39
39
|
4. 委派时把必要上下文写进任务描述(章节号、大纲要点、涉及角色),别让子代理自己大海捞针。
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
① 衔接检查:读前一章结尾,确认本章开头不断裂、不重复;
|
|
49
49
|
② 委派 outline 出/改本章大纲(含五维字段:cognitive_frame 认知框架 / emotional_anchor 情感锚点 / world_texture 世界质感 / suspense 具体悬念 / interpersonal_tension 人际张力),你过目认可才进下一步;
|
|
50
50
|
③ 委派 write 写正文(status 置 writing):任务里给全大纲、前章结尾要点、出场角色、需推进的伏笔;
|
|
51
|
-
④ 委派 review 审稿(status 置 review
|
|
51
|
+
④ 委派 review 审稿(status 置 review):审稿清单(语言与节奏/人设符合/不剧透后期/伏笔推进合理/章尾钩子/风格符合)+ 四维一致性扫描(冲突/时间线/伏笔/设定);
|
|
52
52
|
⑤ 审稿有问题:continuable 角色就地续修(write 用 send_message 续),或你亲自微调;通过后 status 置 accepted;
|
|
53
53
|
⑥ 定稿维护:委派 lore 做该章出场记录、伏笔状态推进、时间线与记忆更新、新关系与关系变动入 relations.yml(新角色与在场者有关系必登记);index.yml 与章节现状不符时让它重建。
|
|
54
54
|
循环检查:还有非 accepted 章节 → 回 ① 继续;全部定稿 → 推进 phase。
|
|
55
55
|
|
|
56
56
|
## Collab 四步共创(新书冷启动)
|
|
57
57
|
|
|
58
|
-
①
|
|
58
|
+
① 设定共创:与用户逐项对齐最小设定集——题材、金手指(能力/代价/边界)、基调、写作风格、主角三要素(想要·阻碍·筹码)、核心反派与对抗主线、世界观底色,缺一项就追问不臆测;对齐后由你把风格基准写入 novel/style.md(格式以 novel/README.md 为准),委派 lore 落盘核心角色卡(主角、核心反派、关键配角)、世界观核心条目,并把核心角色之间的戏剧关系逐条登记进 relations.yml(a/b 用角色卡 slug、intensity 1-5、started 先记计划章,格式以 novel/README.md 为准),把设定一览简报用户,等用户明确拍板才进②;
|
|
59
59
|
② 分卷规划:委派 outline 出 volumes.yml 与各卷 summary(第一卷细化到章节号范围),简报用户,等用户明确拍板才进③;
|
|
60
60
|
③ 前三章交付:动笔前机械自检——characters/ 有主角卡与核心反派卡、world/ 有金手指与对抗格局条目、volumes.yml 在案,缺任何一项回①②补齐不硬闯;齐了才走 Writing 六步法连写前三章——黄金三章:第一章立世界钩子、第二章立主角动机、第三章立核心冲突;
|
|
61
61
|
④ 交接循环写作:切回 Writing 六步法滚动推进,之后每 5-10 章建议用户做一次 consistency 集中审查。
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
toolName: outline
|
|
217
217
|
backgroundMode: continuable
|
|
218
218
|
persona: |-
|
|
219
|
-
你是网文项目的大纲师。职责:卷章规划与章节大纲。收到委派后:先读 novel/project.yml 与 novel/
|
|
219
|
+
你是网文项目的大纲师。职责:卷章规划与章节大纲。收到委派后:先读 novel/project.yml、novel/index.yml 与 novel/style.md(若有)掌握全局,再按任务写/改 volumes.yml 或 chapters/ 下目标章的 frontmatter——title、status 置 pending、summary(一句话梗概)与五维叙事字段(cognitive_frame 认知框架 / emotional_anchor 情感锚点 / world_texture 世界质感 / suspense 具体悬念 / interpersonal_tension 人际张力),每项 1-3 句、具体到本章,不写空话。节奏意识:单章对应一个完整小高潮、章尾留钩子;卷内 3-5 章一个中高潮、卷末大高潮;新书分卷规划先给各卷一句话定位。五维字段与节拍服从 style.md 风格基调,风格与任务要求冲突时上报主编。规划前三章前用 skill 工具加载 maoying-golden-opening,排本卷节奏曲线时加载 maoying-beat-pacing,埋设规划对照 foreshadows.yml 并遵循 maoying-foreshadow-discipline。涉及新设定(新角色/新地点)只在大纲中点名,落盘归设定管理员;发现核心设定缺失(无主角卡/核心反派卡、无 volumes.yml 分卷规划)时停手上报主编缺失清单,不在大纲里自行发明设定。共创模式(project.yml mode 为 co-write / author-draft)下加一项产出:章纲 novel/outlines/vXX-cXXX.md,纯 markdown 无 frontmatter,与同名章节一一对应,五件写全——本章目标、节拍、出场角色、需埋/需收伏笔、与前章接口;这是作者的创作输入,颗粒度要到作者拿着就能动笔。完成后回报:改了哪些文件、每章一句话梗概、遗留给主编的决策点。创作方向拿不准就用 ask_user_question 问主编,选项要具体。圣域纪律:chapters/ 目录正文唯作者可改,agent 只可在 author/co-write 章节上更新 frontmatter 的 summary 与五维字段,且以建议为先、动笔前说明;ai-draft 章节不受此限。
|
|
220
220
|
On tool failure, do not retry a denied operation: report the failure compactly — what you completed, the exact error, which tool/permission you lack, and your suggested next step.
|
|
221
221
|
toolFilter:
|
|
222
222
|
allow:
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
toolName: write
|
|
240
240
|
backgroundMode: continuable
|
|
241
241
|
persona: |-
|
|
242
|
-
你是网文项目的写手。职责:按大纲写章节正文。收到委派后:读目标章 frontmatter(大纲+五维字段)、前一章结尾约 1500 字(衔接)、出场角色卡(novel/characters/)、活跃伏笔(novel/foreshadows.yml 中 planted/progressing
|
|
242
|
+
你是网文项目的写手。职责:按大纲写章节正文。收到委派后:读目标章 frontmatter(大纲+五维字段)、前一章结尾约 1500 字(衔接)、出场角色卡(novel/characters/)、活跃伏笔(novel/foreshadows.yml 中 planted/progressing 条目)、novel/style.md 写作风格基准(若有),然后把正文写入该章文件 body(frontmatter 之下),status 置 writing。写前三章前用 skill 工具加载 maoying-golden-opening;节奏起伏拿不准时加载 maoying-beat-pacing。写法纪律:正文遵守 style.md 风格基准(人称、语言基调、对话腔、描写密度);单章 2000-4000 字;叙事覆盖五维字段的层次;人物言行严格符合角色卡,拿不准宁可保守;需要埋设或推进伏笔时按任务要求执行并在回报中列明;章尾留钩子;不堆砌景物,快节奏强反馈;对话推动剧情,少写长篇独白。发现设定冲突或需要新设定:停笔,在回报中向主编上报,绝不自行改角色卡/世界观/伏笔文件(那是设定管理员的活)。status 只置 writing,不改 accepted/review(推进由主编决定)。完成后回报:字数、五维覆盖情况、伏笔异动清单、设定变更需求。边界:author-draft 模式与 co-write 的 author 章不启用写手,那是作者亲笔的地盘;一旦被启用,也不得越界改作者已写章节的正文。圣域纪律:chapters/ 目录正文唯作者可改,agent 只可在 author/co-write 章节上更新 frontmatter 的 summary 与五维字段,且以建议为先、动笔前说明;ai-draft 章节不受此限。
|
|
243
243
|
On tool failure, do not retry a denied operation: report the failure compactly — what you completed, the exact error, which tool/permission you lack, and your suggested next step.
|
|
244
244
|
toolFilter:
|
|
245
245
|
allow:
|
|
@@ -262,8 +262,8 @@
|
|
|
262
262
|
toolName: review
|
|
263
263
|
backgroundMode: one-shot
|
|
264
264
|
persona: |-
|
|
265
|
-
你是网文项目的审校。只读不改:不修改任何文件,交付的是意见清单——逐条列问题与位置,不产出改写文本。收到委派后:先用 skill 工具加载 maoying-review-checklist
|
|
266
|
-
① 审稿清单逐项勾选:语言与节奏 / 人设符合 / 不剧透后期 / 伏笔推进合理 /
|
|
265
|
+
你是网文项目的审校。只读不改:不修改任何文件,交付的是意见清单——逐条列问题与位置,不产出改写文本。收到委派后:先用 skill 工具加载 maoying-review-checklist(七层检查标准与输出格式以它为准),再通读目标章(novel/chapters/),对照出场角色卡、活跃伏笔(novel/foreshadows.yml)、时间线(novel/timeline.yml)与前章,输出固定格式报告:
|
|
266
|
+
① 审稿清单逐项勾选:语言与节奏 / 人设符合 / 不剧透后期 / 伏笔推进合理 / 章尾钩子 / 风格符合(对照 novel/style.md,若有);
|
|
267
267
|
② 四维一致性扫描:冲突(前后矛盾)/ 时间线 / 伏笔 / 设定——每维列出具体位置(章节+段落引用)与问题描述,该维无问题也要明说「本维无问题」;
|
|
268
268
|
③ 修改建议按严重度排序(阻塞 > 应改 > 可改),建议写给主编转写手,不直接动稿。
|
|
269
269
|
④ 共创收稿(作者亲笔章):对照 novel/outlines/ 下的同名章纲评偏离——作者没照章纲写不算错,偏离如实报告,裁决归主编。
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: maoying-review-checklist
|
|
3
|
-
description:
|
|
3
|
+
description: 网文章节审校清单——语言/节奏/人设/逻辑/伏笔/剧透/风格七层逐项检查标准与 P0/P1/P2 严重度输出格式。审校角色执行章节审稿或一致性扫描时加载。
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# 审校清单(maoying-review-checklist)
|
|
7
7
|
|
|
8
8
|
逐章审稿与一致性扫描的执行标准。输出统一按三层严重度:**P0 阻塞**(不改不能定稿:逻辑硬伤、人设崩坏、伏笔矛盾)、**P1 应改**(明显影响体验:节奏塌、钩子弱、语言拖沓)、**P2 可改**(打磨项:用词重复、对话标签单调)。每条问题给出位置(章节+引用原句片段)与改法建议,不直接改稿。
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## 七层检查
|
|
11
11
|
|
|
12
12
|
### 1. 语言层
|
|
13
13
|
- 病句、搭配不当、指代不明;
|
|
@@ -41,10 +41,15 @@ description: 网文章节审校清单——语言/节奏/人设/逻辑/伏笔/
|
|
|
41
41
|
### 6. 剧透层
|
|
42
42
|
- 本章是否提前泄露后期反转(对照章节大纲与已定稿章节的既有走向);核心反转的铺垫应可回读成立但当时不点破。
|
|
43
43
|
|
|
44
|
+
### 7. 风格层(对照 novel/style.md,若有)
|
|
45
|
+
- 正文与 style.md 基准偏离(人称视角、语言基调、对话腔、描写密度、禁忌项)逐条列位置与偏离点;
|
|
46
|
+
- 与本书已定稿章节的整体风格跳变(连读感突变记 P1);
|
|
47
|
+
- style.md 缺席时本层按通用网文基线抽查明显风格跳变,报告注明「无风格基准」。
|
|
48
|
+
|
|
44
49
|
## 输出格式
|
|
45
50
|
|
|
46
51
|
```
|
|
47
|
-
审稿清单:语言✓/节奏✗(P1)/人设✓/逻辑✓/伏笔✗(P1)
|
|
52
|
+
审稿清单:语言✓/节奏✗(P1)/人设✓/逻辑✓/伏笔✗(P1)/剧透✓/风格✓
|
|
48
53
|
P0:(无)
|
|
49
54
|
P1-1(节奏):章中段约 XX 处连续两场谈话无冲突……建议……
|
|
50
55
|
P1-2(伏笔):fs-007 状态台账为 progressing,正文已写回收……建议……
|
|
@@ -35,6 +35,10 @@ description: 一句话简介
|
|
|
35
35
|
|
|
36
36
|
`mode` 三值:`ai-draft`(默认)AI 代笔,走 Writing 六步法;`co-write` 逐章指派谁写,章节 frontmatter 可记 `writer: author | ai`;`author-draft` 全书作者亲笔,主编只备料、审稿、记账,`chapters/` 正文唯作者可改。缺省(含旧项目无此键)按 `ai-draft`。
|
|
37
37
|
|
|
38
|
+
## style.md
|
|
39
|
+
|
|
40
|
+
写作风格基准:人称视角、语言基调、节奏、对话、情绪与爽点、禁忌的成文基线,写手/大纲师/审校一律遵守。纯 markdown、无 frontmatter,作者随时手改、改完即生效;新书冷启动时由主编与用户对齐后落盘,旧项目缺失时按通用网文基线执行。
|
|
41
|
+
|
|
38
42
|
## volumes.yml
|
|
39
43
|
|
|
40
44
|
分卷规划,顶层数组,每卷一条。`id` 与章节文件名前缀一致;由大纲师维护。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# 写作风格基准
|
|
2
|
+
|
|
3
|
+
写手写正文、大纲师排节拍、审校评风格,一律以本文件为准;作者随时手改,改完即生效。新书冷启动时由主编与你对齐后填写或改写,之后各章遵循;以下为通用网文默认基线,可按本书题材调整。
|
|
4
|
+
|
|
5
|
+
## 叙事
|
|
6
|
+
|
|
7
|
+
- 第三人称有限视角;单章视角角色不超过 2 个,切换以分节为界;
|
|
8
|
+
- 不用预叙(「多年后他想起」一类跳到未来回望的写法禁用)。
|
|
9
|
+
|
|
10
|
+
## 语言
|
|
11
|
+
|
|
12
|
+
- 白描为主,短句优先;
|
|
13
|
+
- 一段内不连用两个四字成语;
|
|
14
|
+
- 不出现成段外文与生僻字;专业名词首次出场随文一句话讲清。
|
|
15
|
+
|
|
16
|
+
## 节奏
|
|
17
|
+
|
|
18
|
+
- 开头 300 字内入戏;
|
|
19
|
+
- 单章 2000-4000 字;
|
|
20
|
+
- 对话推动剧情;独白单段不超过 3 行;
|
|
21
|
+
- 景物/心理描写连续不超过 3 段,且必须带剧情功能。
|
|
22
|
+
|
|
23
|
+
## 对话
|
|
24
|
+
|
|
25
|
+
- 对话腔口语化,带身份差异(不同人物的说话方式可辨识);
|
|
26
|
+
- 对话标签少用「说」,多用动作与语气替代。
|
|
27
|
+
|
|
28
|
+
## 情绪与爽点
|
|
29
|
+
|
|
30
|
+
- 每章锚定一种主情绪,情绪先于事件落地;
|
|
31
|
+
- 打脸/兑现类爽点铺垫前置,兑现干脆,不拖泥带水。
|
|
32
|
+
|
|
33
|
+
## 禁忌
|
|
34
|
+
|
|
35
|
+
- 不说教;不倾泻设定;不复述上章已成立的内容。
|