dsh-maoying 0.1.20 → 0.1.22
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 +4 -4
- package/README.md +3 -3
- package/docs/creator-panel.md +17 -9
- package/docs/design.md +8 -8
- package/docs/spec.md +2 -2
- package/index.mjs +126 -29
- package/lib/base.css +25 -10
- package/lib/client.js +551 -85
- package/lib/epub.mjs +6 -0
- package/lib/novel-parse.mjs +103 -19
- package/lib/panel-core.mjs +16 -7
- package/lib/self-update.mjs +118 -21
- package/lib/version.mjs +30 -2
- package/package.json +2 -1
- package/presets/maoying/agent.cordis.v2.yml +5 -0
- package/presets/maoying/agent.cordis.yml +9 -0
- package/presets/maoying/restrict.mjs +16 -3
- package/scripts/smoke-client.mjs +128 -0
package/README.en.md
CHANGED
|
@@ -76,7 +76,7 @@ Open "**毛颖创作台**" at the bottom of the DSH sidebar — a full-page cock
|
|
|
76
76
|
- **Chapters**: status filters + the writing queue; click a chapter for full-page reading — back button, outline side-by-side (Markdown rendered), in-place editing with save for the manuscript and the outline (textarea with manual save; if the file was changed externally while editing, saving is rejected and the latest content restored, never overwriting manual edits), 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, and its header offers "Edit source file" for whole-file editing of volumes.yml.
|
|
78
78
|
- **Managed-file surfaces**: character / worldbuilding / science cards and the six ledgers (relations / timeline / memories / clue board / foreshadows / volumes) can all be edited from the panel — "New" on the wall header, "Edit source file" / "Delete file" on card details, "Edit source file" on each ledger card head (empty timelines and relations graphs keep the entry too); saving runs structural validation (ledgers must parse as entry arrays, entity cards must carry frontmatter fences — anything else is rejected), and external-change protection matches the prose editor; the chapter reader can create an outline (pre-filled with the five-part skeleton), delete the outline, and delete the chapter — deletions always go through a confirm dialog: irreversible, recovery is via git, and the static check will flag orphaned references.
|
|
79
|
-
- **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
|
+
- **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); a "Check updates" button re-checks the latest online version on demand (bypassing the 1-hour cache — the panel only auto-checks once when opened, so a long-lived page never goes deaf); the gear opens Settings — UI language (中文/English), six font-size steps (100%–150%), all applied immediately and remembered.
|
|
80
80
|
|
|
81
81
|
The panel writes state (chapter status, project.yml, index.yml rebuilds), and every human editing entry — prose, outlines, ledgers, entity cards — is for humans only (hash-based stale-write detection plus structural validation; external changes are never overwritten); the agent boundary is unchanged: in co-writing, prose belongs to the author and the chief.
|
|
82
82
|
|
|
@@ -153,12 +153,12 @@ On DSH 0.1.7+ also delete the dsh-maoying managed block in `~/.dsh/cordis.patch.
|
|
|
153
153
|
|
|
154
154
|
## Compatibility
|
|
155
155
|
|
|
156
|
-
At startup the plugin reads the host version
|
|
156
|
+
At startup the plugin reads the host version and picks the delegation-line shape automatically — **after a host upgrade, a restart is all it takes; no reinstall**. Version detection is a five-probe chain (argv[1] → full argv scan → app.asar / desktop-runtime → Electron API → home-global fallback); the first four probes anchor the real host of the running process, and the `<home>/node_modules/@deepseek-ai/dsh/package.json` home-global copy is only the last resort (a stale CLI leftover may lag several major versions). Each probe's trace is inspectable via the creator panel's `host-debug` endpoint (paths sanitized):
|
|
157
157
|
|
|
158
158
|
- **DSH 0.1.7 – 0.2.1-rc.1 → v1 inline shape**: the 5 role delegation lines inline persona and toolFilter; the managed block is 1 declaration line.
|
|
159
|
-
- **DSH 0.2.1-rc.2+ → v2 AgentPreset shape**: from that version dsh-tool-subagent rejects inline persona/toolFilter, so the plugin switches to [presets/maoying/agent.cordis.v2.yml](presets/maoying/agent.cordis.v2.yml) — the managed block writes 6 declaration lines (
|
|
159
|
+
- **DSH 0.2.1-rc.2+ → v2 AgentPreset shape**: from that version dsh-tool-subagent rejects inline persona/toolFilter, so the plugin switches to [presets/maoying/agent.cordis.v2.yml](presets/maoying/agent.cordis.v2.yml) — the managed block writes 6 declaration lines (editor-in-chief preset + 5 role presets: outliner / writer / reviewer / lore keeper / researcher); the chief's delegation line points at the role presets via `agentPreset`, and each role's tool surface is carried by the preset's entity tool lines (reviewer read-only, researcher with web search).
|
|
160
160
|
- **Role presets appear in the preset selector** (the host has no hiding mechanism for presets); their names carry "(内部)" to mark them: in daily writing they are picked automatically via chief delegation, but you can also select a writer/researcher directly for a session without the chief.
|
|
161
|
-
- Both shapes share persona text byte-for-byte from one source ([test/composition.test.mjs](test/composition.test.mjs) guards it); if the host version can't be read, the plugin
|
|
161
|
+
- Both shapes share persona text byte-for-byte from one source ([test/composition.test.mjs](test/composition.test.mjs) guards it); if the host version can't be read at all (all five probes miss), the plugin writes no declaration line and self-heals via the partial path — it never declares a possibly ≥0.2.1-rc.2 host as v1 inline, and the block is written automatically on the next start once the version is readable.
|
|
162
162
|
- The ≤0.1.6 directory-scan track: not supported (the host dropped it).
|
|
163
163
|
|
|
164
164
|
## License
|
package/README.md
CHANGED
|
@@ -73,10 +73,10 @@ npm install -g dsh-maoying # 或装成全局 maoying 命令
|
|
|
73
73
|
|
|
74
74
|
DSH 左侧栏底部点「**毛颖创作台**」打开全页面板(改文件、跑流程之外,给肉眼看的驾驶舱):
|
|
75
75
|
|
|
76
|
-
- **章节**:状态筛选 + 按单写作清单;点章节进整页阅读——返回按钮、章纲对照(Markdown
|
|
76
|
+
- **章节**:状态筛选 + 按单写作清单;点章节进整页阅读——返回按钮、章纲对照(Markdown 渲染)、正文与章纲就地编辑保存(正文 textarea 自动保存——待写空章节打开即直接进编辑器、约 2 秒防抖落盘、回车换行自动加段首两个全角空格缩进;章纲 textarea 手动保存;文件在编辑期间被外部改过时拒绝保存并放回磁盘最新内容,不覆盖人工改动)、五维叙事字段、一键状态流转。
|
|
77
77
|
- **概览**:书稿卡右上「编辑」按钮展开 project.yml 的书名/作者/题材/篇幅/语言/工作流阶段/共创模式/简介表单化编辑(原「小说设置」tab 并入),保存保留文件里原有注释;表单载入后 project.yml 被外部(作者手改 / agent 推进阶段)改过时拒绝保存并提示重改,不覆盖人工改动。概览另设「分卷规划」卡,列出 volumes.yml 各卷定位与计划章节范围——拍板过的分卷有处可查,卡头「编辑源文件」可整文件编辑 volumes.yml。
|
|
78
78
|
- **受管文件写面**:角色/设定/科学设定卡与六本账本(关系/时间线/记忆/线索板/伏笔/分卷)均可从面板编辑源文件——墙头「新建」、卡详情「编辑源文件/删除文件」、各账本卡头「编辑源文件」(时间线/关系图空态也给入口);保存时结构校验(账本须是条目数组、实体卡须带 frontmatter 围栏,不合即拒存),外部改动保护与正文编辑同口径;章节阅读页可新建章纲(本章目标/节拍/出场角色/需埋需收伏笔/与前章接口五件骨架预填)、删除章纲与删除章节——删除均确认弹层,不可逆,后悔靠 git,静态体检会把孤儿引用标出来。
|
|
79
|
-
- **顶栏**:当前版本号常驻;npm 上有新版时亮「有新版 ↗」徽章直连发布页,旁边「一键升级」按钮直接用 pnpm 把最新包装进当前 DSH 档(仅支持 profile 目录安装,pnpm 缺失时自动回落 corepack;检测到全局 `maoying` CLI 时顺带以同一精确版本更新,未装则跳过;装完需重启 DeepSeek
|
|
79
|
+
- **顶栏**:当前版本号常驻;npm 上有新版时亮「有新版 ↗」徽章直连发布页,旁边「一键升级」按钮直接用 pnpm 把最新包装进当前 DSH 档(仅支持 profile 目录安装,pnpm 缺失时自动回落 corepack;检测到全局 `maoying` CLI 时顺带以同一精确版本更新,未装则跳过;装完需重启 DeepSeek 才生效);「检测更新」按钮随时手动现测线上最新版(绕过 1h 缓存——面板打开时只自动拉一次,页面开久了也不聋);齿轮打开系统设置,界面语言中文/English 可切、字号六档(100%–150%)整体缩放,均立即生效并记忆。
|
|
80
80
|
|
|
81
81
|
面板写状态(章节状态、project.yml、index.yml 重建),正文/章纲/账本/实体卡等全部人工编辑入口只给人用(hash 旧版检测 + 结构校验,外部改动一律拒绝覆盖)——agent 的创作边界不变:共创模式下正文永远归作者与主编。
|
|
82
82
|
|
|
@@ -158,7 +158,7 @@ rm -rf "${DSH_HOME:-$HOME/.dsh}/skills/"maoying-*
|
|
|
158
158
|
- **DSH 0.1.7 – 0.2.1-rc.1 → v1 内联形状**:5 个角色委派行直接内联 persona 与 toolFilter,托管块 1 条声明行。
|
|
159
159
|
- **DSH 0.2.1-rc.2+ → v2 AgentPreset 形状**:该版本起 dsh-tool-subagent 拒绝内联 persona/toolFilter,插件改用 [presets/maoying/agent.cordis.v2.yml](presets/maoying/agent.cordis.v2.yml)——托管块写 6 条声明行(主编 preset + 大纲师/写手/审校/设定管理员/素材调研员 5 张角色 preset),主编委派行以 `agentPreset` 指向角色 preset,角色的工具面由 preset 里的实体工具行承载(审校只读、考据带联网检索)。
|
|
160
160
|
- **角色预设会出现在预设选择器**(宿主对预设没有隐藏机制),name 带「(内部)」标明:日常写作由主编委派自动选用,不经主编、想单独开一个写手/考据会话时直接选中即可。
|
|
161
|
-
- 两种形状的 persona 文本同源逐字一致([test/composition.test.mjs](test/composition.test.mjs)
|
|
161
|
+
- 两种形状的 persona 文本同源逐字一致([test/composition.test.mjs](test/composition.test.mjs) 逐字节把关);宿主版本完全未知(五级探针全败)时不写声明行、预设文件照落走 partial 自愈,绝不把可能已 ≥0.2.1-rc.2 的宿主声明成 v1 内联(下次启动版本可读时自动补写)。
|
|
162
162
|
- ≤0.1.6 的目录扫描轨道:不做(宿主已淘汰)。
|
|
163
163
|
|
|
164
164
|
## 许可
|
package/docs/creator-panel.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## 0. 定位与边界
|
|
6
6
|
|
|
7
|
-
面板管**可视化、状态,以及作者本人的稿件编辑**:小说内容的生产仍归主编(agent)与作者在编辑器/共创流程里协作,面板负责「一眼看清全局 +
|
|
7
|
+
面板管**可视化、状态,以及作者本人的稿件编辑**:小说内容的生产仍归主编(agent)与作者在编辑器/共创流程里协作,面板负责「一眼看清全局 + 状态快捷流转」,同时给作者本人一个带护栏的正文/章纲编辑入口(正文 textarea 自动保存、章纲手动保存 + hash 旧版检测,见 §2 的 `chapter-save` / `outline-save`;正文自动保存见 §4.1「正文编辑直开 + 自动保存 + 段首缩进」条目)。0.1.20 起(§4.1「全量可编辑写面」)六本账本 yml、四目录实体卡 md 与章纲新建也开放人工编辑,编辑纪律统一为「保存时结构校验(解析失败/缺围栏 422 拒存)+ 保存后语义体检(`check`)+ git 回滚兜底」——语义规则(状态机、必填章号等)是 agent 纪律不是人工禁区,但写面只服务人。agent 的创作边界不变:共创模式下 agent 依旧不得碰 author 章节的正文(spec §作者稿圣域)——面板编辑是**人**用的刀,不是给 agent 开的新通道。
|
|
8
8
|
|
|
9
9
|
## 1. 包接线(单包形态,不建子包)
|
|
10
10
|
|
|
@@ -27,7 +27,7 @@ export const inject = ['webServer', 'workspaceRegistry'] // webRuntime 经 ctx
|
|
|
27
27
|
webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req, res) => {...} })
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
- **信任围栏**:isTrustedApiRequest 信任围栏(Host 回环或 webRuntime.trustedHosts + 拒 cross-site + origin 同源校验),403 走 `{ error: 'forbidden' }
|
|
30
|
+
- **信任围栏**:isTrustedApiRequest 信任围栏(Host 回环或 webRuntime.trustedHosts + 拒 cross-site + origin 同源校验),403 走 `{ ok: false, error: 'forbidden' }`(错误形状与全局契约同形,客户端形状检查不必对这一条网开一面)。读取 trustedHosts 每请求实时 `ctx.get('webRuntime')?.trustedHosts ?? []`,自捕不抛。
|
|
31
31
|
- duplicate prefix route 短重试(8 次 × 30ms)。
|
|
32
32
|
- 所有响应 JSON;错误形状统一 `{ ok: false, error }`(读路由可省 ok,直接 `{ error }` + 状态码)。
|
|
33
33
|
- **路径安全**:凡是带 `path` 参数的请求,必须 resolve 后与 workspaceRegistry 列出的某个 workspace root **严格相等**(不做子路径、不做 ~ 展开);novel 目录恒为 `<root>/novel`,file 参数 basename 化 + 规范化后必须落在 novel 根内(防穿越)。不满足一律 403。
|
|
@@ -38,8 +38,8 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
38
38
|
| 路由 | 方法 | 入参 | 出参 |
|
|
39
39
|
|---|---|---|---|
|
|
40
40
|
| `client.css` | GET | — | text/css(lib/base.css 现读,no-cache) |
|
|
41
|
-
| `update-check` | GET |
|
|
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
|
|
41
|
+
| `update-check` | GET | `fresh=1`(可选) | `{ ok, current, latest, updateAvailable, releaseUrl, checkedAt, error, source }`——新版检测(npm registry → npmmirror → GitHub 三级源,1h 缓存 + 7 天 stale-if-error,全败静默降级为 latest=null / error 非空);`fresh=1` 为手动检测:绕过 1h 缓存强制现测(getVersionInfo `force`,in-flight 已在跑则复用——它本就是缓存失效后的新检测),超时由 3s 放宽到 10s(用户在前端等结果) |
|
|
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)、装后对账不符(verify-failed:装完回读安装位 package.json 的 version 与精确目标不一致,message 带手动重装出路)一律 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 契约) |
|
|
@@ -119,25 +119,25 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
119
119
|
```
|
|
120
120
|
|
|
121
121
|
解析层:`lib/panel-core.mjs` 新建,**与 CLI 共享**——把 bin/maoying.mjs 里的 frontmatter 解析 / CJK 计数 / collectChapters / index.yml 重建抽到 `lib/novel-parse.mjs`,CLI 与面板都 import(CLI 行为零变化,改动后跑一遍 CLI 验收)。YAML 解析走 index.mjs 现有 loadYamlLib(dsh home 的 yaml 包),不可用时回退:frontmatter 用正则平键解析,数组 yml 用「条目块 + 平键」极简解析(顶层只允许数组或平键——数据规范本来就禁深嵌套),解析不了的条目跳过并记 `warnings`。
|
|
122
|
-
所有实体字段缺省为 null/[],绝不让一个坏文件炸掉整个快照;坏文件进 `warnings: ["<file>: <原因>"]`,面板顶部黄条显示。
|
|
122
|
+
所有实体字段缺省为 null/[],绝不让一个坏文件炸掉整个快照;坏文件进 `warnings: ["<file>: <原因>"]`,面板顶部黄条显示。yaml 真/降级解析也各有告警不再静默:yaml 包在场但真解析失败时,先记 `"<file>: yaml 解析失败(…),已降级极简解析"` 再降级;极简解析丢弃的零缩进散键行同样挂上文件名并入 warnings。
|
|
123
123
|
|
|
124
124
|
## 4. 浏览器侧:面板结构
|
|
125
125
|
|
|
126
126
|
挂载:
|
|
127
127
|
- 左侧栏底部动作位入口「毛颖创作台」(sidebar.footer.action 键控槽,ctx.effect 注入 + MutationObserver 自愈);
|
|
128
128
|
- 会话中栏全页 overlay:`html[data-dsh-maoying-active]` 驱动,`dsh-panel-activate` 事件与 mnemon/taskboard/ssh 等全页面板互斥,点侧栏会话行自动关闭;
|
|
129
|
-
- 顶部工作区选择器(workspaces 路由取数,hasNovel 的排前;无 novel 工作区显示引导卡:一句 `maoying init` 提示 + 复制按钮)。面板懒挂载、关闭不卸载(React 状态全保留),故每次打开都借 `dsh-panel-activate` 广播三重重拉——workspaces(外部新建/删除的工作区重开即见)、整份 state 快照(CLI / agent 改动的章节与设定重开即最新,无需手点右上角刷新)、阅读页本章正文(经重开序号透传 ChapterReader
|
|
129
|
+
- 顶部工作区选择器(workspaces 路由取数,hasNovel 的排前;无 novel 工作区显示引导卡:一句 `maoying init` 提示 + 复制按钮)。面板懒挂载、关闭不卸载(React 状态全保留),故每次打开都借 `dsh-panel-activate` 广播三重重拉——workspaces(外部新建/删除的工作区重开即见)、整份 state 快照(CLI / agent 改动的章节与设定重开即最新,无需手点右上角刷新)、阅读页本章正文(经重开序号透传 ChapterReader 静默重拉;编辑中的草稿不受影响,仍只在 409 冲突出口「载入磁盘版」被重置)。
|
|
130
130
|
|
|
131
131
|
九个 tab(顺序即默认排布):
|
|
132
132
|
|
|
133
133
|
1. **概览**:project 卡(书名大字 + 类型/篇幅/语言 pills + 阶段徽章 + 简介,卡右上「编辑」按钮展开书稿元信息表单——原「小说设置」tab 并入,见 §4.1「小说设置并入概览」条目);统计卡(总字数/章节数/状态分布条);最近更新(chapters 按 mtime 前 5);warnings 黄条;静态体检黄条(check 路由问题清单,见 §4.1「人工改动保护 + 静态体检黄条」(0.1.14)条目);分卷规划卡卡头「编辑源文件」(volumes.yml 整文件编辑弹层,0.1.20 写面)。
|
|
134
|
-
2. **章节**:表格(卷-章/标题/状态徽章/字数/梗概),状态筛选 pills;表格上方「按单写作」区块——有章纲且 status=pending 的章按序列出(章纲节选 + 状态徽章),未建章的孤儿章纲照列(「未建章」徽章 + 「建章」一键:POST chapter-create 落 pending 空章并重拉快照;行点击开章纲全文弹层,0.1.15 起),共创模式的主工作清单;行点击开整页阅读视图(0.1.12 起替代原右侧抽屉——fixed 侧弹层会被 DSH 侧边栏遮挡):返回按钮 + GET chapter 正文 + 章纲对照栏(左右分栏或折叠段)+ 复制文件路径按钮 + 五维字段卡 + 状态流转按钮(当前态 → 下一态一键,pending→writing→review→accepted);正文与章纲对照栏均可就地编辑保存(
|
|
134
|
+
2. **章节**:表格(卷-章/标题/状态徽章/字数/梗概),状态筛选 pills;表格上方「按单写作」区块——有章纲且 status=pending 的章按序列出(章纲节选 + 状态徽章),未建章的孤儿章纲照列(「未建章」徽章 + 「建章」一键:POST chapter-create 落 pending 空章并重拉快照;行点击开章纲全文弹层,0.1.15 起),共创模式的主工作清单;行点击开整页阅读视图(0.1.12 起替代原右侧抽屉——fixed 侧弹层会被 DSH 侧边栏遮挡):返回按钮 + GET chapter 正文 + 章纲对照栏(左右分栏或折叠段)+ 复制文件路径按钮 + 五维字段卡 + 状态流转按钮(当前态 → 下一态一键,pending→writing→review→accepted);正文与章纲对照栏均可就地编辑保存(POST chapter-save / outline-save 携 base_hash 409 人工改动检测——frontmatter 围栏逐字节保留,外部改动拒写不落盘、草稿保留并亮「载入磁盘版(放弃草稿)/基于最新版重改」两个出口,0.1.19 起、两出口见 §4.1 末条):正文编辑态自动保存(约 2s 防抖,0.1.22 起——写盘内容即新基线、成功不退出编辑态、纯缩进占位不落盘),待写空章节(body 全空白)打开即直接进编辑态、不再经「编辑」按钮中转,回车换行自动加段首两个全角空格缩进,编辑器排版与只读正文一致(所见即所得)并随内容自动长高;章纲对照栏仍手动保存(textarea + 取消/保存);0.1.20 写面:无章纲空态加「新建章纲」(outlines/<stem>.md 五件骨架预填)、章纲对照栏卡头「删除章纲」、阅读顶栏「删除章节」——后两者均确认弹层(不可逆,后悔靠 git,check 会标孤儿引用)。
|
|
135
135
|
3. **角色**:卡墙(姓名/role 徽章四色/年龄性别/性格一句/动机一行),点击看全卡(含 arc/markers/background);0.1.20 写面:墙头「新建角色」(模板预填)+ 全卡弹层底部「编辑源文件/删除文件」。
|
|
136
136
|
4. **伏笔**:四列看板(埋设/推进/已回收/弃用),卡片含 priority 色点(high 红/normal 灰/low 淡)、`source: backfill` 的「回填」小徽章(账本回填来的条目,区别于写前规划)、埋设章→预期回收章、notes 全文直显(0.1.10 去掉两行截断);卡片按钮:推进到下一状态(planted→progressing→resolved;abandoned 只能从看板拖不动、按钮给「弃用」)。列头计数;0.1.20 写面:卡头「编辑源文件」(foreshadows.yml)。
|
|
137
137
|
5. **关系图**:canvas 力导向图——节点=角色(radius 随出场无关、固定 18px,名字 label 下方),边=relations(intensity 1-5 → 线宽 1-4px,type 标在边中点 hover 显示 description);节点按角色配色(0.1.20 起):主角 #f53f3f 红 / 配角 #165dff 蓝 / 反派 #1d2129 黑 / 龙套与未标注 #fadc19 黄(查表 miss 落 minor,全节点恒有色),拖拽固定态统一加粗描边;脚注带四色图例;拖拽节点、空白处滚轮缩放、双击复位;右上角 +/- 缩放按钮(钳制同滚轮 0.4-2.5)与全屏按钮(Fullscreen API 能力检测,不支持则不渲染;进入全屏走 ResizeObserver 自动重铺);intensity 缺省 3。角色多于 30 个时只画与 relations 相连的节点,其余进「未入图」列表。防重叠(0.1.20 起):节点最小间距 62 硬碰撞分离(含名字 label 纵向净空,力式斥力之上兜底,弹簧 rest 120→130 配合多留缝隙);边为二次曲线——按两侧空旷度选朝向(迟滞防抖)绕开途中节点,同一对角色的重复边偏移放大 + 交替朝向互相分开(type 标签随曲线移到 t=0.5 点);hover 命中按曲线几何采样。0.1.20 写面:卡头「编辑源文件」(relations.yml,空态同样给入口)。
|
|
138
138
|
6. **时间线**:垂直轴,importance 1-5 → 点大小/颜色深浅,year 左列右卡片;0.1.20 写面:卡头「编辑源文件」(timeline.yml,空态同样给入口——账本空不等于不可编辑)。
|
|
139
139
|
7. **设定**:子 tab 三分区切换(世界观/科学设定/资料检索,各带条目计数,仅渲染当前激活分区)——world 内多分类时二级子 tab 按 category 切换(每 tab 带条目计数,仅渲染激活分类;单分类/无分类平铺直出,见 §4.1「世界观分类二级子 tab」条目);science 三色标签(known 绿/extrapolation 黄/hypothesis 紫,标签下 references 链接列表);research 逐条渲染,空态也常驻可切;0.1.20 写面:world/science 组头「新建设定」(模板预填)+ 条目卡「编辑源文件/删除文件」,research 暂无写面入口(快照有卡,入口下轮再议)。
|
|
140
|
-
8. **写作风格**:novel/style.md 写作风格基准的查看与编辑——视图态 MdView 渲染全文,卡头「编辑」进整文件编辑态(textarea + 取消/保存,保存 POST style-save 携 base_hash 409
|
|
140
|
+
8. **写作风格**:novel/style.md 写作风格基准的查看与编辑——视图态 MdView 渲染全文,卡头「编辑」进整文件编辑态(textarea + 取消/保存,保存 POST style-save 携 base_hash 409 人工改动保护,冲突时草稿保留、亮「载入磁盘版/基于最新版重改」两出口,写后新指纹接下一轮基线);文件缺失时显示空态卡,可用插件内置默认基线一键预填创建(保存即落盘),空文本允许保存。
|
|
141
141
|
9. **索引板**:clues 按 kind 分组(线索/红鲱鱼/推理/疑问,resolved 划线置灰)+ memories 按 category 分组(promise 类高亮);0.1.20 写面:两卡头「编辑源文件」(clue-board.yml / memories.yml)。
|
|
142
142
|
|
|
143
143
|
状态徽章色(全局统一):pending 灰 / writing 蓝 / review 橙 / accepted 绿;phase 七态沿用七色 pill。文案全中文、禁 emoji 图标(用 ui-primitives 图标或纯 CSS)。
|
|
@@ -169,7 +169,15 @@ webServer.register({ kind: 'prefix', path: '/api/maoying', handler: async (req,
|
|
|
169
169
|
- **关系图防重叠**(0.1.20):canvas 力导向图三处——①硬碰撞分离:step() 速度积分之后按最小间距 62(两节点半径 18×2 + 名字 label 纵向净空)做两轮位置级互推(固定/拖拽节点不动推对方、双固定跳过,位移不写回速度靠阻尼收敛),弹簧 rest 120→130 配合多留缝隙,节点永不压叠;②边改二次贝塞尔曲线:控制点垂直偏移随边长缩放钳 [12,28],draw 每帧按曲线两侧对无关节点的最小距离(9 点采样)比空旷度选朝向、另一侧明显更空(+12 世界像素)才翻转(side 挂边对象上迟滞防抖),同一对角色的重复边按 (a,b) 分组序号放大偏移并交替朝向互相分开,type 标签移到曲线 t=0.5 点;③hover 命中检测跟曲线几何走(二次贝塞尔 9 段折线近似,阈值仍 10 世界像素,edgeCtrl 与 draw 同源)。纯前端,路由不变;I18N 不变。注:边完全零交叉在数学上不总成立(非平面图),本改动目标是消除节点压叠、线穿节点、重复边重合与常见交叠,不保证零交叉。
|
|
170
170
|
- **关系图工具角标**(0.1.19):关系图右上角新增工具条——+/- 缩放按钮(步进 ×1.25 / ÷1.25,钳制与滚轮同口径 0.4-2.5;React 按钮够不着绘图 effect 闭包里的 sim,经 zoomApiRef 桥调用,effect 清理时置 null 防旧闭包误触)与全屏切换按钮(Fullscreen API 能力检测——wrap 有 requestFullscreen/webkitRequestFullscreen 且 document.fullscreenEnabled 非假才渲染,否则不占位;监听 fullscreenchange/webkitfullscreenchange 同步「全屏/退出全屏」文案,浏览器 ESC 退出也跟上;进/出全屏 wrap 尺寸变化走既有 ResizeObserver 自动 resize 重铺 canvas,Chromium UA 全屏样式覆盖 480px 定高)。纯前端,路由不变;I18N zh/en 各 260 → 264 条(relations.zoomIn / zoomOut / fs / exitFs),relations.help 文案同步更新(提缩放按钮与右上角全屏)。
|
|
171
171
|
- **世界观分类二级子 tab**(0.1.20):设定 tab 世界观条目分区内部再进一层——原纵向分组列表(myt-listHead 分组头 + myt-worldGroup 堆叠)在 world 条目按 category(frontmatter 自由文本,缺省「未分类」)聚出**多个**分类时改为二级子 tab 条(复用 myt-subTabs/myt-subTab + myt-subTabsInner 修饰:字号/padding 收一号),每 tab 带条目计数、仅渲染激活分类的条目(条目 markup 不变);单分类或全部未分类时平铺直出,不出 tab 条也不出分组头,world 为空维持空态。激活分类记忆在组件 state(切走再切回不丢),快照重拉后所存分类已不存在时回落第一个。category 是数据文件自由文本,分组容器保持 Object.create(null) 防原型污染。纯前端,路由不变;I18N zh/en 各 264 → 265 条(world.catTabsAria)。
|
|
172
|
-
- **全量可编辑写面(0.1.20)**:面板剩余只读内容开放为人工可编辑——①路由 19 → 23 条:`file-source`(GET 原文+指纹)/`file-save`(POST 整文件覆写,base_hash 409 + 结构校验 422)/`file-create`(新建,已存在 409)/`file-delete`(删除,服务端不二次确认),四条共用 `managedRelCheck` 白名单(§2 受管文件白名单表):六个账本 yml 读/存、四个 md 实体目录读/存/建/删(slug=`^[a-z0-9][a-z0-9-]*$`)、outlines 读/存/建/删、chapters 仅删;`index.yml`/`project.yml`/`novel/style.md` 有意排除(各走专用路由/弹层)。②结构校验 `validateManagedText`:账本 yml 须解析为顶层数组且条目全为平键对象(快照会静默丢弃 null/裸标量/数组条/空对象条——存得进读不出等于丢数据,存时一并 422);md 实体卡只验围栏成对;回退解析与真 yaml 解析两条路同一结论(测试双口径断言)。③UI 九处入口:角色墙头「新建角色」+ 全卡弹层「编辑源文件/删除文件」;world/science 组头「新建设定」(模板预填 `---\ncategory: 地点\n---` / `---\nlabel: known\n---`)+ 条目卡编辑/删除;伏笔/关系图/时间线/索引板两卡卡头「编辑源文件」(时间线/关系图空态也给入口);概览分卷卡头「编辑源文件」(volumes.yml);章节阅读页无章纲空态「新建章纲」(五件骨架预填)+ 阅读顶栏「删除章节」+ 章纲卡头「删除章纲」(删除均确认弹层,文案点名不可逆、后悔靠 git、check 会标孤儿引用)。共用三弹层组件(SourceEditorModal 带请求序号防过期响应 / CreateFileModal slug 客户端预校 / ConfirmModal 在途禁双击)。research 快照有卡但 v1 无写面入口。I18N zh/en 各 265 → 294 条(src.* 20 + toast.fileSaved/fileCreated/fileDeleted + reader.createOutline/deleteChapter + outline.deleteBtn + char.createBtn + world.createBtn/createScienceBtn)。测试:panel-core 层
|
|
172
|
+
- **全量可编辑写面(0.1.20)**:面板剩余只读内容开放为人工可编辑——①路由 19 → 23 条:`file-source`(GET 原文+指纹)/`file-save`(POST 整文件覆写,base_hash 409 + 结构校验 422)/`file-create`(新建,已存在 409)/`file-delete`(删除,服务端不二次确认),四条共用 `managedRelCheck` 白名单(§2 受管文件白名单表):六个账本 yml 读/存、四个 md 实体目录读/存/建/删(slug=`^[a-z0-9][a-z0-9-]*$`)、outlines 读/存/建/删、chapters 仅删;`index.yml`/`project.yml`/`novel/style.md` 有意排除(各走专用路由/弹层)。②结构校验 `validateManagedText`:账本 yml 须解析为顶层数组且条目全为平键对象(快照会静默丢弃 null/裸标量/数组条/空对象条——存得进读不出等于丢数据,存时一并 422);md 实体卡只验围栏成对;回退解析与真 yaml 解析两条路同一结论(测试双口径断言)。③UI 九处入口:角色墙头「新建角色」+ 全卡弹层「编辑源文件/删除文件」;world/science 组头「新建设定」(模板预填 `---\ncategory: 地点\n---` / `---\nlabel: known\n---`)+ 条目卡编辑/删除;伏笔/关系图/时间线/索引板两卡卡头「编辑源文件」(时间线/关系图空态也给入口);概览分卷卡头「编辑源文件」(volumes.yml);章节阅读页无章纲空态「新建章纲」(五件骨架预填)+ 阅读顶栏「删除章节」+ 章纲卡头「删除章纲」(删除均确认弹层,文案点名不可逆、后悔靠 git、check 会标孤儿引用)。共用三弹层组件(SourceEditorModal 带请求序号防过期响应 / CreateFileModal slug 客户端预校 / ConfirmModal 在途禁双击)。research 快照有卡但 v1 无写面入口。I18N zh/en 各 265 → 294 条(src.* 20 + toast.fileSaved/fileCreated/fileDeleted + reader.createOutline/deleteChapter + outline.deleteBtn + char.createBtn + world.createBtn/createScienceBtn)。测试:panel-core 层 7 例(白名单矩阵/白名单外与形状违规拒/结构校验双口径/读存建删)+ 路由层 2 例(file-source、三态全链路)。
|
|
173
|
+
|
|
174
|
+
- **Markdown 管道表格**(0.1.20):零依赖渲染器补 GFM 管道表格——表头行 + `|---|---|` 分隔行 + 数据行,设定卡/章纲里写 `| 职级 | 修为档位 |` 不再渲染成带竖线的纯文本段落;对齐语法 `:---`/`---:`/`:---:` 落 `myt-ta-l/c/r` class(th/td 共用,不用 inline style),转义管道 `\|` 切列豁免(哨兵回填字面 `|`)、首尾外圈竖线丢弃、**表头列数须与分隔行列数相等才认表**(GFM 规范——否则「含 `|` 的段落行 + `---` 分隔线」会被误吞成表头)、数据行列数不齐宽容渲染(缺列补空 td、多列照渲染),段落兜底补表格起始检测(段落后紧跟表格时截断成块,GitHub 语义)。表格包 `myt-mdTableWrap` 宽表横向滚动;`myt-mdTable` 显式 `white-space: normal` 覆盖落点容器(`.myt-outlineBody`/`.myt-worldDesc`)的 pre-wrap,否则单元格内容按源码换行。安全底线不变:先整体转义再变换,单元格文本走既有已转义行 + inlineMd,不自建未转义通道。纯前端,路由不变;I18N 不变;新增 test/md-table.test.mjs(源码切片 `new Function` 求值 renderMd 纯函数直测,切片标记失效带定位报错防静默漏测)。
|
|
175
|
+
|
|
176
|
+
- **手动检测更新**:`update-check` 只在面板打开时拉一次(服务端另有 1h 缓存),页面开久了拿不到新判定,升级提示成聋——顶栏版本号旁新增「检测更新」描边胶囊按钮(与「一键升级」同形 `.myt-verCheck`,点击后变「检测中…」并禁用防连点,完成恢复),点击带 `fresh=1` 请求现测:服务端 getVersionInfo 走 `force` 先把模块级缓存压空再走正常流程(**force 只保证不命中 1h TTL 内的陈旧 cache,不保证「另发一轮全新外网请求」**——若已有 in-flight 检测在跑则复用等它落定,它本就是缓存失效后发起的新检测,有意语义),手动检测超时由 3s 放宽到 10s(用户在前端等结果),缺省调用维持 3s 不变。结果分流:updateAvailable → setVer 更新版本状态(「有新版 ↗」徽章与「一键升级」按钮自然出现)+ toast「发现新版 {latest}」;无新版 → toast「已是最新版({current})」(复用 toast.selfUpdate.upToDate,与一键升级短路分支同一文案,不添重复键);失败(服务端 error 非空或请求 reject)→ toast「检测失败:{首句}」——三路全败 error 是长串排障文案,取首句、仍超 ~80 字符截断加省略号防糊屏。前端直连 maoyingApi 不走 apiGet:apiGet 会先统一 toast 遍错误再 rethrow,叠加「检测失败」就双份提示(与 runSelfUpdate 直连同一理由);手动检测请求超时沿用统一 60s(服务端 10s 即收敛)。测试:lib 层 force 两例(预填缓存 → TTL 内命中 → force 换桩重测且 fetch 计数增加;并发双 force 复用同一 in-flight 只发一轮)+ 路由层 fresh=1 分支接线(query 解析 + 契约形状)。纯前端加参数扩展,路由仍 23 条;I18N zh/en 各 294 → 298 条(header.checkUpdate / header.checkingUpdate / toast.selfUpdate.found / toast.selfUpdate.checkFail)。
|
|
177
|
+
|
|
178
|
+
- **写面 409 冲突两出口**:正文(chapter-save)/章纲(outline-save)/受管文件(file-save)/写作风格(style-save)四处编辑保存遇 409 外部改动冲突时,不再静默重拉把草稿重置掉——草稿绝不丢:冲突提示下方亮「载入磁盘版(放弃草稿)/基于最新版重改」两个出口,前者重拉磁盘真值回填编辑器并重置基线,后者只把基线换成磁盘最新(拿新 hash),草稿原样留在编辑器、改完再存即用新基线;重拉失败冲突态保留、出口仍在可重试。project-save 表单维持原「409 重拉放回磁盘真值」路径(表单无草稿语义)。纯前端,路由不变;I18N zh/en 各 298 → 301 条(edit.conflict409 / edit.conflictDrop / edit.conflictKeep)。同轮服务端契约补齐:围栏 403 与全部错误统一 `{ ok: false, error }` 形状(routes 测试有断言);self-update 新增失败 kind `verify-failed`(装后对账不符,见 §2 路由表);快照 warnings 补 yaml 真/降级解析告警(见 §3)。
|
|
179
|
+
|
|
180
|
+
- **正文编辑直开 + 自动保存 + 段首缩进**(0.1.22):章节阅读页正文编辑三处升级——①**空章节直开编辑态**:待写章节(body 全空白)打开即直接进正文编辑态,不再经顶部「编辑」按钮中转;预填段首两个全角空格让首段也有缩进,基线与预填同值(不判脏,退出确认与自动保存均不误触);正在编辑绝不打扰(409 放弃草稿等编辑中重拉被 editingRef 守卫排除),同章本轮只自动进一次,切章/重开面板算新一轮可再进,手动「取消」退出后本轮不再自动重进。②**正文自动保存**:编辑中改动 2s 防抖落盘(调度 effect 无 dep 数组每轮渲染重排定时器,连续打字永远差 2s 才保存;409 冲突/保存在途不排),调用点快照 draft 防在途打字被误标已保存,**写盘内容即新基线**(连续自动保存不假脏),成功不退出编辑态、不清草稿,编辑 hint 缀「已自动保存(HH:MM:SS)」时刻标;纯缩进占位(trim 为空)不落盘;409 人工改动检测语义不变——冲突即停排、草稿保留、两出口照旧。③**回车自动段首缩进**:编辑器内回车换行自动插入两个全角空格(U+3000×2,网文段落缩进惯例),光标落到缩进之后(draft 提交后下一轮渲染恢复选区);IME 组态中的回车(keyCode 229 / isComposing)是选词确认不劫持,取不到 selectionStart 的退化环境交给浏览器默认行为。编辑器排版与只读正文一致(同 UI 字体、13px/22px 行高,所见即所得)并随内容自动长高(≥320px,overflow hidden + resize none),主题色边框保留可辨识仍在编辑;脏退出确认兜底不动(自动保存窗口之外切章/取消/返回仍先确认)。纯前端,路由不变;I18N zh/en 各 301 → 302 条(reader.autoSaved;reader.bodyEditHint 文案同步改写)。
|
|
173
181
|
|
|
174
182
|
## 5. 验收基线
|
|
175
183
|
|
package/docs/design.md
CHANGED
|
@@ -53,7 +53,7 @@ dsh-maoying/
|
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
- 全程幂等:标记块已存在且版本一致则跳过;不一致则整块替换。YAML 解析/序列化运行时从 dsh home 惰性借宿主自带的 `yaml` 包(lib/yaml-loader.mjs 多级探针),借不到回退内置极简解析(JSON 单行回退同样合法 YAML)——仓库零 npm 依赖,dependencies 恒为空。
|
|
56
|
-
- 按宿主版本自动选委派行形状:≥0.2.1-rc.2 写 v2 AgentPreset 六行块(agent.cordis.v2.yml
|
|
56
|
+
- 按宿主版本自动选委派行形状:≥0.2.1-rc.2 写 v2 AgentPreset 六行块(agent.cordis.v2.yml),确知 <0.2.1-rc.2 写 v1 内联单行块;宿主版本完全未知(五级探针全败)时不写声明行、预设文件照落走 partial 自愈(下次启动版本可读时自动补写)——绝不把可能已 ≥0.2.1-rc.2 的宿主声明成 v1 内联。宿主升降级重启即整块切换,不再需要任何降级提示(形态依据见 §8,支持面见 §6)。
|
|
57
57
|
3. 卸载 `dsh plugin remove dsh-maoying` 后:bundle 行消失,托管块与 .agent-presets/maoying/ 需手动删(README 写明两条 rm 命令)。
|
|
58
58
|
|
|
59
59
|
## 2. 功能裁决(一期范围)
|
|
@@ -116,15 +116,15 @@ novel/
|
|
|
116
116
|
|
|
117
117
|
| toolName | 角色 | 干什么 | backgroundMode | 工具面(toolFilter.allow) |
|
|
118
118
|
|---|---|---|---|---|
|
|
119
|
-
| outline | 大纲师 | 卷章规划、章节大纲+五维字段 | continuable | read read_image glob grep write edit bash ask_user_question todo_write |
|
|
120
|
-
| write | 写手 | 章节正文(按大纲写、衔接前章、回报设定变更) | continuable | read read_image glob grep write edit bash ask_user_question todo_write |
|
|
121
|
-
| review | 审校 | 审稿清单 + 一致性扫描(冲突/时间线/伏笔/设定 + 连贯六维,只读、出报告不改稿) | one-shot | read read_image glob grep todo_write ask_user_question |
|
|
122
|
-
| lore | 设定管理员 | 章节/角色/世界观/伏笔/时间线/记忆/线索板维护,出场记录与伏笔状态推进 | continuable | read glob grep write edit bash ask_user_question todo_write |
|
|
119
|
+
| outline | 大纲师 | 卷章规划、章节大纲+五维字段 | continuable | read read_image glob grep write edit bash skill ask_user_question todo_write |
|
|
120
|
+
| write | 写手 | 章节正文(按大纲写、衔接前章、回报设定变更) | continuable | read read_image glob grep write edit bash skill ask_user_question todo_write |
|
|
121
|
+
| review | 审校 | 审稿清单 + 一致性扫描(冲突/时间线/伏笔/设定 + 连贯六维,只读、出报告不改稿) | one-shot | read read_image glob grep skill todo_write ask_user_question |
|
|
122
|
+
| lore | 设定管理员 | 章节/角色/世界观/伏笔/时间线/记忆/线索板维护,出场记录与伏笔状态推进 | continuable | read glob grep write edit bash skill ask_user_question todo_write |
|
|
123
123
|
| research | 素材调研员 | 联网考据(历史/地理/专业领域),笔记落 novel/research/ | one-shot | web_search web_fetch glob grep read write bash ask_user_question todo_write |
|
|
124
124
|
|
|
125
|
-
主编工具面(restrict.mjs 白名单):outline write review lore research + send_message list_agents interrupt_agent + glob grep read read_image bash + todo_write ask_user_question get_goal create_goal update_goal exit_plan_mode job_output job_list job_kill
|
|
125
|
+
主编工具面(restrict.mjs 白名单):outline write review lore research + send_message list_agents interrupt_agent + glob grep read read_image bash + todo_write ask_user_question get_goal create_goal update_goal exit_plan_mode job_output job_list job_kill + edit(主编亲笔小修小补不过委派)。其中 'write' 一名双义——单条 'write' 同时放行 tool-fs 的文件写工具与 delegation-write 以 toolName: write 注册的委派工具(白名单按工具名匹配,宿主 registry 对同名工具的处理仓内无法核实,改名前必须先核实)。registry deny: ['skill'](堵 ~1.3k 技能目录注入)。
|
|
126
126
|
|
|
127
|
-
注意:toolFilter.allow 一律不放 mcp__* 工具(子代理创建时 tools.restrict 校验名字必须在注册面,MCP 是否挂载是 host 事实,硬编码会让未装 tavily
|
|
127
|
+
注意:toolFilter.allow 一律不放 mcp__* 工具(子代理创建时 tools.restrict 校验名字必须在注册面,MCP 是否挂载是 host 事实,硬编码会让未装 tavily 的宿主直接建不出子代理);skill 仅用于按 persona 加载 maoying-* 方法论技能(审校同样只读不改)。
|
|
128
128
|
|
|
129
129
|
delegation-labels 静态角色名映射:{outline: 大纲师, write: 写手, review: 审校, lore: 设定管理员, research: 素材调研员}。
|
|
130
130
|
|
|
@@ -172,7 +172,7 @@ persona prefix 内容骨架(agent.cordis.yml 内联,中文书写):
|
|
|
172
172
|
- ✅ **技能包(黄金三章/爽点节奏/审校清单/伏笔管理)**——已做:skills/ 四目录随包分发,安装器复制到 `<home>/skills/maoying-*`(逐字节比较幂等 + .installed-version 标记,见 index.mjs installSkills)。
|
|
173
173
|
- ✅ **导出(MD/TXT/EPUB)**——已做:`maoying export [dir] --format md|txt|epub [--out <目录>]`;EPUB 由 lib/epub.mjs 零依赖打包(手写 stored zip + CRC32 表格法,EPUB 3 最小合法结构)。
|
|
174
174
|
- ✅ **0.2.1-rc.2+ AgentPreset 形态适配**——已做:presets/maoying/agent.cordis.v2.yml(主编 + 5 角色 preset 的 v2 组合源)+ index.mjs composeManagedRows 按宿主版本双形态切换(v1 内联 / v2 AgentPreset,宿主升降级重启即整块换形状;防漂移由 test/composition.test.mjs 逐字节断言,见 README「兼容面」)。
|
|
175
|
-
- ✅ **Web 创作台面板**——已做:/api/maoying/*
|
|
175
|
+
- ✅ **Web 创作台面板**——已做:/api/maoying/* 二十三条路由 + 左侧栏入口 + 会话中栏全页面板,九视图(概览(含书稿元信息编辑)/章节/角色/伏笔看板/关系图/时间线/设定/写作风格/索引板);设计与契约见 docs/creator-panel.md。
|
|
176
176
|
|
|
177
177
|
## 9. 五期:共创模式(作者亲笔稿)
|
|
178
178
|
|
package/docs/spec.md
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
- `suspense` 具体悬念:本章悬念与章尾钩子的问题化表述(必须是「读者会问出的问题」,不是「作者知道的事实」)。
|
|
46
46
|
- `interpersonal_tension` 人际张力:本章人物关系中的压力与变化(谁与谁的矛盾升温/缓和/变质)。
|
|
47
47
|
- 五维字段在大纲阶段(outline 角色)写定、写作阶段(write 角色)覆盖、审校阶段(review 角色)对照——三处都用同一组键,这就是「按大纲写」的可检查形态。
|
|
48
|
-
- 可选 `writer` 键(co-write 用):`author | ai`,记本章正文归谁写;缺省按 project.yml 的 `mode` 推——`author-draft` 下整本按 author、`ai-draft` 下整本按 ai
|
|
48
|
+
- 可选 `writer` 键(co-write 用):`author | ai`,记本章正文归谁写;缺省按 project.yml 的 `mode` 推——`author-draft` 下整本按 author、`ai-draft` 下整本按 ai。键只做记录与消费:`maoying brief` 写作卡「本章状态」节照录原值、`maoying check` 校验越枚举(合法值 `author|ai`);不进创作台面板快照(快照 chapters 无此键),状态机仍看 `status`。
|
|
49
49
|
|
|
50
50
|
### outlines/vXX-cXXX.md
|
|
51
51
|
章纲:共创模式(`co-write` / `author-draft`)下大纲师给作者的创作输入,与章节文件一一对应(`outlines/v01-c005.md` ↔ `chapters/v01-c005.md`)。**纯 markdown、无 frontmatter**——作者会在任意编辑器里打开它,`---` 围栏只会碍眼(第 1 节「frontmatter 用围栏」的总约束不适用本目录)。六件内容写全:本章目标、节拍(场景推进顺序与情绪起伏)、出场角色(谁上场、各带什么目的)、需埋/需收伏笔(对照 foreshadows.yml 报 id)、与前章接口(从哪个钩子接)、章尾钩子——独立成 `## 钩子` 节(钩子类型 + 内容,类型对照 maoying-beat-pacing 的钩子类型库,写明下一章前提被什么改变)。颗粒度标准:作者拿着就能动笔,不写「参见大纲」式的空转。
|
|
@@ -139,6 +139,6 @@ total: {chapters: 21, words: 65210, accepted: 18}
|
|
|
139
139
|
- `maoying index` 是第 3 节重建逻辑的人工兜底(主编的日常路径是让 lore 角色直接重建);
|
|
140
140
|
- `maoying export [dir]` 导出全书为 md / txt / epub(默认 md;`--out` 改输出目录,缺省 `<dir>/exports/`);
|
|
141
141
|
- `maoying brief [dir] <chapter>` 组装单章写作卡(章纲 + 前章结尾 + 出场角色卡 + 活跃伏笔);`--out <文件>` 落盘,缺省打到 stdout;
|
|
142
|
-
- `maoying check [dir]` 静态体检(不调模型):frontmatter
|
|
142
|
+
- `maoying check [dir]` 静态体检(不调模型):frontmatter 完整性与重复键(重复 warn)、枚举值(章节 status/writer、伏笔 status/source、记忆 category、线索 kind、project mode/phase)、章号卷号一致、伏笔账本全项(planted/resolved 章号引用存在性、id 重复、状态自洽——resolved 必填 resolved_chapter 且不得早于 planted、expected_resolve 只查格式不查存在性)、伏笔超期未推进(planted 超 30 章无进展即 warn)、volumes id 重复、章纲钩子小节缺失、时间线年份与 chapter 引用、记忆 category 与 source_chapter 引用、线索 kind 与 related_chapter 引用、关系 started/ended 引用、AI 味统计(连续同式开头/比喻词密度/句长波动,命中即 warn),并把本节 phase 完成判据落成静态检查——phase 到 outline 及之后仍缺主角卡/核心反派卡/世界观条目/分卷规划,或有两张及以上角色卡而 relations.yml 为空,即报 error(判据不满足回上一阶段补齐)。
|
|
143
143
|
|
|
144
144
|
六者都零依赖,解析器只认本规范的两类形态(平键映射、顶层数组);深度嵌套不在支持范围——这是规范约束的执行手段之一。
|
package/index.mjs
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
* ≥ 0.2.1-rc.2 用 v2 AgentPreset 形状(agent.cordis.v2.yml,6 条声明行——
|
|
33
33
|
* 主编 preset + 5 张角色 preset,委派行以 agentPreset 指向角色 preset,该版
|
|
34
34
|
* 本起 dsh-tool-subagent 拒绝内联形状)。宿主升降级后重启即整块切换,无需
|
|
35
|
-
* 重装插件(见 composeManagedRows
|
|
35
|
+
* 重装插件(见 composeManagedRows)。版本完全未知(五级探针全败)时不写
|
|
36
|
+
* 声明行、走 partial 自愈——绝不把可能已 ≥ 0.2.1-rc.2 的宿主声明成 v1 内联。
|
|
36
37
|
*
|
|
37
38
|
* YAML:解析与序列化都从 <home>/node_modules 惰性加载 yaml 包(dsh 自带 v2,
|
|
38
39
|
* 本包零 npm 依赖)。agent.cordis.yml / agent.cordis.v2.yml 解析前先中和
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
* 限额 JSON 读体、client.css 现读下发。
|
|
52
53
|
*/
|
|
53
54
|
|
|
54
|
-
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
55
|
+
import { closeSync, copyFileSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, readSync, renameSync, statSync, unlinkSync, writeFileSync } from 'node:fs'
|
|
55
56
|
import { createHash } from 'node:crypto'
|
|
56
57
|
import { pathToFileURL } from 'node:url'
|
|
57
58
|
import { getVersionInfo } from './lib/version.mjs'
|
|
@@ -507,11 +508,15 @@ export function parsePresetRows(yaml, agentCordisV2Text) {
|
|
|
507
508
|
* - 宿主 ≥ 0.2.1-rc.2(hostRejectsInlineDelegation)→ 'v2':解析
|
|
508
509
|
* agent.cordis.v2.yml,返回 6 条声明行(主编 preset + 5 张角色 preset,
|
|
509
510
|
* 委派行 agentPreset 形状);
|
|
510
|
-
* -
|
|
511
|
-
* persona/toolFilter
|
|
512
|
-
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
511
|
+
* - 宿主 < 0.2.1-rc.2(确知旧版)→ 'v1':现有单行组装路径原样(内联
|
|
512
|
+
* persona/toolFilter 形状);
|
|
513
|
+
* - 版本完全未知(五级探针全败)→ 不组装:rows 返回 null、reason
|
|
514
|
+
* 'version-unknown'。真实宿主若 ≥ 0.2.1-rc.2,dsh-tool-subagent 会拒绝
|
|
515
|
+
* v1 内联形状(五路委派全废、只能等探测成功才自愈),「不声明」比
|
|
516
|
+
* 「声明成 v1」安全;shape 仍保守记 'v1'(仅名义标签,本次不落盘)。
|
|
517
|
+
* yaml 包不可用或组合源解析失败时同样 rows 返回 null(shape/hostVersion 照常
|
|
518
|
+
* 带回,reason 区分 'yaml-unavailable' / 'compose-failed' / 'version-unknown'),
|
|
519
|
+
* 由调用方走既有的「只落文件、跳过声明行并 warn、下次自愈」分支。
|
|
515
520
|
*/
|
|
516
521
|
/**
|
|
517
522
|
* preset 声明行里的本地插件名换成 file:// 绝对地址。相对名按「声明方 baseUrl」
|
|
@@ -544,6 +549,14 @@ export function composeManagedRows(home, logger = console) {
|
|
|
544
549
|
const hostVersion = readHostVersion(home)
|
|
545
550
|
const useV2 = hostVersion !== null && hostRejectsInlineDelegation(hostVersion)
|
|
546
551
|
const shape = useV2 ? 'v2' : 'v1'
|
|
552
|
+
// 版本完全未知(五级探针全败)时保持「不声明」而不是「声明成 v1 内联」:
|
|
553
|
+
// 若真实宿主 ≥ 0.2.1-rc.2,v1 内联声明行会被 dsh-tool-subagent 整批拒绝,
|
|
554
|
+
// 五路委派全废且探测成功前无法自愈。走既有的 partial 分支(预设文件照落、
|
|
555
|
+
// warn 留痕、下次宿主启动自动补写),与 yaml-unavailable 同一自愈语义。
|
|
556
|
+
// 确知宿主 < 0.2.1-rc.2 时 v1 内联仍照写(下方原路径不变)。
|
|
557
|
+
if (hostVersion === null) {
|
|
558
|
+
return { shape, rows: null, hostVersion, yaml: null, reason: 'version-unknown' }
|
|
559
|
+
}
|
|
547
560
|
const yaml = loadYamlLib(home)
|
|
548
561
|
if (!yaml) return { shape, rows: null, hostVersion, yaml: null, reason: 'yaml-unavailable' }
|
|
549
562
|
try {
|
|
@@ -668,6 +681,39 @@ export function upsertManagedBlock(dshHome, blockText, { versionMarkerCurrent, l
|
|
|
668
681
|
return span !== null ? 'updated' : 'created'
|
|
669
682
|
}
|
|
670
683
|
|
|
684
|
+
/**
|
|
685
|
+
* 清扫托管块临时件残留:upsertManagedBlock 在 writeFileSync 与 renameSync 之间
|
|
686
|
+
* 崩溃(kill -9 / 断电)时,`.cordis.patch.yml.tmp-<pid>` 会永远留在 dsh home。
|
|
687
|
+
* 仅在插件启动(apply 早期)顺带跑一次,纪律:
|
|
688
|
+
* - 只认本插件的临时件命名(`.cordis.patch.yml.tmp-` 前缀 + 纯数字 pid 后缀,
|
|
689
|
+
* 与 upsertManagedBlock 的命名一字不差),其余文件一概不碰;
|
|
690
|
+
* - mtime 超过 1 小时才删——正常写盘从建临时件到 rename 是毫秒级,1h 窗口
|
|
691
|
+
* 绝不误杀并发进程正在写的临时件;
|
|
692
|
+
* - 全程自吞错(目录读不到 / 单个 stat/unlink 失败都跳过):清扫是顺手卫生,
|
|
693
|
+
* 绝不能影响宿主启动,也不为它打日志刷屏。
|
|
694
|
+
* @returns {number} 本次删除的残留临时件个数(测试与观察用)
|
|
695
|
+
*/
|
|
696
|
+
export function sweepStaleManagedBlockTmp(dshHome, { now = Date.now(), maxAgeMs = 3_600_000 } = {}) {
|
|
697
|
+
let names
|
|
698
|
+
try {
|
|
699
|
+
names = readdirSync(dshHome)
|
|
700
|
+
} catch {
|
|
701
|
+
return 0 // home 不存在/读不到:无事可扫
|
|
702
|
+
}
|
|
703
|
+
let removed = 0
|
|
704
|
+
for (const name of names) {
|
|
705
|
+
if (!/^\.cordis\.patch\.yml\.tmp-\d+$/.test(name)) continue
|
|
706
|
+
try {
|
|
707
|
+
if (now - statSync(path.join(dshHome, name)).mtimeMs <= maxAgeMs) continue
|
|
708
|
+
unlinkSync(path.join(dshHome, name))
|
|
709
|
+
removed += 1
|
|
710
|
+
} catch {
|
|
711
|
+
/* 单个探测/删除失败跳过——下个启动周期再试 */
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return removed
|
|
715
|
+
}
|
|
716
|
+
|
|
671
717
|
// ── 方法论技能落盘 ──────────────────────────────────────────────────────────
|
|
672
718
|
|
|
673
719
|
/** 技能源目录(包内 skills/)。 */
|
|
@@ -899,28 +945,37 @@ function json(res, status, payload) {
|
|
|
899
945
|
}
|
|
900
946
|
|
|
901
947
|
// 错误原文(可能带本机绝对路径)只进日志;回给浏览器的 message 先把插件
|
|
902
|
-
//
|
|
948
|
+
// 安装根与目录锚点替换成 ~ 再说。
|
|
903
949
|
const SANITIZE_HOME_DIR = path.resolve(os.homedir())
|
|
904
950
|
const SANITIZE_PLUGIN_ROOT = PLUGIN_ROOT.replace(/[\\/]+$/, '')
|
|
951
|
+
// DSH_HOME 可指向家目录外(如 /opt/dsh)——它的绝对路径与家目录同级敏感,
|
|
952
|
+
// 同样不出门,进替换表。等于文件系统根时置 null 跳过(split('/') 会毁掉
|
|
953
|
+
// 整条文本,宁可不脱敏也不能把响应毁成乱码)。
|
|
954
|
+
const SANITIZE_DSH_HOME = (() => {
|
|
955
|
+
const resolved = path.resolve(resolveDshHome())
|
|
956
|
+
return resolved === path.sep ? null : resolved.replace(/[\\/]+$/, '')
|
|
957
|
+
})()
|
|
905
958
|
|
|
906
959
|
function sanitizeMessage(message) {
|
|
907
960
|
let text = String(message ?? '')
|
|
908
|
-
// 先长后短:安装根通常是 home 的子路径,先替换更具体的
|
|
961
|
+
// 先长后短:安装根通常是 home/DSH_HOME 的子路径,先替换更具体的
|
|
909
962
|
text = text.split(SANITIZE_PLUGIN_ROOT).join('~')
|
|
910
963
|
text = text.split(SANITIZE_HOME_DIR).join('~')
|
|
964
|
+
if (SANITIZE_DSH_HOME !== null) text = text.split(SANITIZE_DSH_HOME).join('~')
|
|
911
965
|
return text
|
|
912
966
|
}
|
|
913
967
|
|
|
914
968
|
/**
|
|
915
|
-
* 递归脱敏:结构里所有字符串值过一遍 sanitizeMessage
|
|
969
|
+
* 递归脱敏:结构里所有字符串值过一遍 sanitize(默认模块级 sanitizeMessage,
|
|
970
|
+
* 可传路由层的 sanitizeWithRoots 把已注册工作区根并入替换表)。host-debug
|
|
916
971
|
* 响应用——trace.via / runtime.argv / execPath / cwd 等带本机绝对路径的字段,
|
|
917
972
|
* 与错误 message 同一纪律:诊断信息照给,本机路径不出门。
|
|
918
973
|
*/
|
|
919
|
-
function sanitizeDeep(value) {
|
|
920
|
-
if (typeof value === 'string') return
|
|
921
|
-
if (Array.isArray(value)) return value.map(sanitizeDeep)
|
|
974
|
+
function sanitizeDeep(value, sanitize = sanitizeMessage) {
|
|
975
|
+
if (typeof value === 'string') return sanitize(value)
|
|
976
|
+
if (Array.isArray(value)) return value.map((v) => sanitizeDeep(v, sanitize))
|
|
922
977
|
if (value !== null && typeof value === 'object') {
|
|
923
|
-
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, sanitizeDeep(v)]))
|
|
978
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, sanitizeDeep(v, sanitize)]))
|
|
924
979
|
}
|
|
925
980
|
return value
|
|
926
981
|
}
|
|
@@ -964,13 +1019,37 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
964
1019
|
const trustedHostsOf = () => {
|
|
965
1020
|
try { return ctx.get('webRuntime')?.trustedHosts ?? [] } catch { return [] }
|
|
966
1021
|
}
|
|
1022
|
+
// 纵深加固(本文件可达处):模块级 sanitizeMessage 的替换表只认 homedir /
|
|
1023
|
+
// 插件根 / DSH_HOME;已注册工作区根是 ctx 级事实(registry 可枚举、运行时
|
|
1024
|
+
// 可增删),且 sanitizeMessage 定义在本文件而非不可改的 panel-core.mjs,
|
|
1025
|
+
// 故在路由层包一层把注册根并入替换表——工作区根落在家目录外时,host-debug
|
|
1026
|
+
// trace 与错误 message 里的绝对路径同样不出门。只包本前缀路由内的使用点;
|
|
1027
|
+
// registry 读不到按「无根可换」继续,脱敏层绝不炸请求(围栏内仅回环受信
|
|
1028
|
+
// 客户端可见,此层属纵深而非唯一防线)。
|
|
1029
|
+
const workspaceRoots = () => {
|
|
1030
|
+
try {
|
|
1031
|
+
return (ctx.workspaceRegistry?.list?.() ?? [])
|
|
1032
|
+
.map((entry) => (typeof entry?.path === 'string' && entry.path.trim() !== '' ? path.resolve(entry.path) : null))
|
|
1033
|
+
.filter((root) => root !== null && root !== path.sep)
|
|
1034
|
+
.sort((a, b) => b.length - a.length) // 先长后短:嵌套根先替换更具体的
|
|
1035
|
+
} catch {
|
|
1036
|
+
return []
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
const sanitizeWithRoots = (message) => {
|
|
1040
|
+
let text = sanitizeMessage(message)
|
|
1041
|
+
for (const root of workspaceRoots()) text = text.split(root).join('~')
|
|
1042
|
+
return text
|
|
1043
|
+
}
|
|
967
1044
|
|
|
968
1045
|
return registerPrefixRouteWithRetry(webServer, {
|
|
969
1046
|
kind: 'prefix',
|
|
970
1047
|
path: '/api/maoying',
|
|
971
1048
|
handler: async (req, res) => {
|
|
972
1049
|
if (!isTrustedApiRequest(req, trustedHostsOf())) {
|
|
973
|
-
|
|
1050
|
+
// 错误形状契约(本文件 §路由要点:「错误形状统一 { ok: false, error }」):
|
|
1051
|
+
// 围栏 403 也带 ok:false,客户端形状检查不必对这一条网开一面。
|
|
1052
|
+
json(res, 403, { ok: false, error: 'forbidden' })
|
|
974
1053
|
return
|
|
975
1054
|
}
|
|
976
1055
|
try {
|
|
@@ -991,7 +1070,7 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
991
1070
|
res.end(css)
|
|
992
1071
|
} catch (err) {
|
|
993
1072
|
ctx.logger?.warn?.('maoying 创作台: client.css 读取失败: ' + String(err?.message ?? err))
|
|
994
|
-
json(res, 500, { ok: false, error:
|
|
1073
|
+
json(res, 500, { ok: false, error: sanitizeWithRoots(`client.css 不可用: ${err?.message ?? err}`) })
|
|
995
1074
|
}
|
|
996
1075
|
return
|
|
997
1076
|
}
|
|
@@ -999,7 +1078,14 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
999
1078
|
if (route === 'update-check' && req.method === 'GET') {
|
|
1000
1079
|
// 新版检测(模仿 dsh-paoding):三级源 + 缓存都在 lib/version.mjs,
|
|
1001
1080
|
// 全失败也静默降级——这里只做转发,绝不影响面板主流程。
|
|
1002
|
-
|
|
1081
|
+
// fresh=1(query,写法同 workspaces/check 路由的 searchParams):
|
|
1082
|
+
// 手动检测更新,绕过 1h 缓存强制现测(getVersionInfo force);
|
|
1083
|
+
// 用户在前端等结果,超时由 3s 放宽到 10s。缺省维持常态 3s。
|
|
1084
|
+
const fresh = url.searchParams.get('fresh') === '1'
|
|
1085
|
+
json(res, 200, {
|
|
1086
|
+
ok: true,
|
|
1087
|
+
...(await getVersionInfo(fresh ? { force: true, timeoutMs: 10000 } : { timeoutMs: 3000 })),
|
|
1088
|
+
})
|
|
1003
1089
|
return
|
|
1004
1090
|
}
|
|
1005
1091
|
|
|
@@ -1009,7 +1095,7 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
1009
1095
|
// 安全边界见 lib/self-update.mjs)。可预期的失败(dev-link /
|
|
1010
1096
|
// unsupported-layout / profile-missing / no-target / pnpm-missing /
|
|
1011
1097
|
// 超时 / 退出码非零 nonzero)统一 HTTP 200 + ok:false,kind 给客户端
|
|
1012
|
-
// 分流、message 经
|
|
1098
|
+
// 分流、message 经 sanitizeWithRoots 脱敏后透出;成功带 restartRequired:
|
|
1013
1099
|
// true——装完的只是磁盘上的新包,运行中的进程仍是旧代码,必须重启
|
|
1014
1100
|
// DeepSeek 才生效,客户端文案必须照实转述。本地已不落后线上时短路
|
|
1015
1101
|
// kind: 'up-to-date'(不装 profile)。两个成功路径都顺带全局 CLI:
|
|
@@ -1024,7 +1110,7 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
1024
1110
|
restartRequired: true, globalCli: r.globalCli ?? null,
|
|
1025
1111
|
})
|
|
1026
1112
|
} else {
|
|
1027
|
-
json(res, 200, { ok: false, kind: r.kind, error:
|
|
1113
|
+
json(res, 200, { ok: false, kind: r.kind, error: sanitizeWithRoots(r.message ?? r.kind) })
|
|
1028
1114
|
}
|
|
1029
1115
|
return
|
|
1030
1116
|
}
|
|
@@ -1062,18 +1148,18 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
1062
1148
|
registryDump.presets = jsonSafe(rows)
|
|
1063
1149
|
}
|
|
1064
1150
|
} catch (e) { registryDump.error = String(e?.message ?? e) }
|
|
1065
|
-
// 出口统一过 sanitizeDeep
|
|
1066
|
-
//
|
|
1151
|
+
// 出口统一过 sanitizeDeep + 工作区根扩展表:trace.via / argv / execPath /
|
|
1152
|
+
// cwd 带本机绝对路径,诊断照给、路径脱敏(与错误 message 同一纪律)。
|
|
1067
1153
|
json(res, 200, {
|
|
1068
1154
|
ok: true,
|
|
1069
1155
|
hostVersion: dbg.version,
|
|
1070
|
-
trace: sanitizeDeep(dbg.trace),
|
|
1156
|
+
trace: sanitizeDeep(dbg.trace, sanitizeWithRoots),
|
|
1071
1157
|
shape,
|
|
1072
1158
|
registry: registryDump,
|
|
1073
1159
|
runtime: {
|
|
1074
|
-
argv: sanitizeDeep(process.argv),
|
|
1075
|
-
execPath:
|
|
1076
|
-
cwd:
|
|
1160
|
+
argv: sanitizeDeep(process.argv, sanitizeWithRoots),
|
|
1161
|
+
execPath: sanitizeWithRoots(process.execPath),
|
|
1162
|
+
cwd: sanitizeWithRoots(process.cwd()),
|
|
1077
1163
|
electron: process.versions?.electron ?? null,
|
|
1078
1164
|
node: process.version,
|
|
1079
1165
|
},
|
|
@@ -1262,7 +1348,7 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
1262
1348
|
if (err?.responded) return
|
|
1263
1349
|
json(res, Number.isInteger(err?.statusCode) ? err.statusCode : 500, {
|
|
1264
1350
|
ok: false,
|
|
1265
|
-
error:
|
|
1351
|
+
error: sanitizeWithRoots(err?.message ?? String(err)),
|
|
1266
1352
|
})
|
|
1267
1353
|
}
|
|
1268
1354
|
},
|
|
@@ -1277,7 +1363,7 @@ export async function registerCreatorRoutes(ctx) {
|
|
|
1277
1363
|
* 2) .installed-version 标记(内容与当前版本一致即不重写);
|
|
1278
1364
|
* 3) 方法论技能落盘到 <home>/skills/maoying-*(不依赖 yaml,独立可失败);
|
|
1279
1365
|
* 4) home patch 托管块(依赖 yaml;形态随宿主版本,见 composeManagedRows;
|
|
1280
|
-
* yaml
|
|
1366
|
+
* yaml 缺席、组合源坏或宿主版本探测全败时跳过并 warn,下次启动自愈)。
|
|
1281
1367
|
* 返回 { status, home, presetDir, copied, skillsCopied, blockAction, reason,
|
|
1282
1368
|
* shape, hostVersion },status 取 'ok'(全链路完成)| 'partial'(托管块未写,
|
|
1283
1369
|
* 原因见 reason);shape 为本次所选委派行形态('v1' | 'v2');skillsCopied 为
|
|
@@ -1328,8 +1414,12 @@ export function ensurePresetInstalled(ctx = {}) {
|
|
|
1328
1414
|
composed.reason === 'yaml-unavailable'
|
|
1329
1415
|
? 'maoying-installer: <home>/node_modules 下找不到 yaml 包,无法解析预设组合——' +
|
|
1330
1416
|
'预设文件已复制,但 cordis.patch.yml 声明行本次跳过,下次宿主启动自动补写'
|
|
1331
|
-
:
|
|
1332
|
-
|
|
1417
|
+
: composed.reason === 'version-unknown'
|
|
1418
|
+
? 'maoying-installer: 宿主版本探测全败,无法安全判定委派行形状' +
|
|
1419
|
+
'(误写 v1 内联会被 0.2.1-rc.2+ 宿主拒绝、五路委派全废)——' +
|
|
1420
|
+
'预设文件已复制,但 cordis.patch.yml 声明行本次跳过,下次宿主启动自动补写'
|
|
1421
|
+
: 'maoying-installer: 预设组合源解析失败——预设文件已复制,但 cordis.patch.yml 声明行本次跳过,' +
|
|
1422
|
+
'修复后下次宿主启动自动补写(详见上方 warn)',
|
|
1333
1423
|
)
|
|
1334
1424
|
return {
|
|
1335
1425
|
status: 'partial',
|
|
@@ -1375,6 +1465,13 @@ export function ensurePresetInstalled(ctx = {}) {
|
|
|
1375
1465
|
*/
|
|
1376
1466
|
export function apply(ctx = {}, _config = {}) {
|
|
1377
1467
|
const logger = ctx?.logger ?? console
|
|
1468
|
+
// 启动顺带清扫上次崩溃可能残留的托管块临时件(见 sweepStaleManagedBlockTmp;
|
|
1469
|
+
// 自吞错、只认本插件命名前缀且 mtime>1h,绝不影响启动)。
|
|
1470
|
+
try {
|
|
1471
|
+
sweepStaleManagedBlockTmp(resolveDshHome())
|
|
1472
|
+
} catch {
|
|
1473
|
+
/* 防御性兜底:函数本身不抛,这里再兜一层 */
|
|
1474
|
+
}
|
|
1378
1475
|
try {
|
|
1379
1476
|
ensurePresetInstalled({ logger })
|
|
1380
1477
|
} catch (error) {
|