dsh-maoying 0.1.19 → 0.1.21

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/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'
@@ -64,6 +65,8 @@ import { fileURLToPath } from 'node:url'
64
65
 
65
66
  import {
66
67
  createChapterFromOutline,
68
+ createManagedFile,
69
+ deleteManagedFile,
67
70
  listWorkspaces,
68
71
  mutateChapterBody,
69
72
  mutateChapterStatus,
@@ -73,10 +76,12 @@ import {
73
76
  mutateProject,
74
77
  parseStateSnapshot,
75
78
  readChapter,
79
+ readManagedFile,
76
80
  readOutlineFile,
77
81
  readStyleState,
78
82
  rebuildIndex,
79
83
  resolveNovelRoot,
84
+ saveManagedFile,
80
85
  saveStyleText,
81
86
  } from './lib/panel-core.mjs'
82
87
  // 静态体检(check 路由)与 CLI check 同源:直接复用 novel-parse 的检查器。
@@ -503,11 +508,15 @@ export function parsePresetRows(yaml, agentCordisV2Text) {
503
508
  * - 宿主 ≥ 0.2.1-rc.2(hostRejectsInlineDelegation)→ 'v2':解析
504
509
  * agent.cordis.v2.yml,返回 6 条声明行(主编 preset + 5 张角色 preset,
505
510
  * 委派行 agentPreset 形状);
506
- * - 否则(含版本读不到的保守侧)→ 'v1':现有单行组装路径原样(内联
507
- * persona/toolFilter 形状)。
508
- * yaml 包不可用或组合源解析失败时 rows 返回 null(shape/hostVersion 照常带回,
509
- * reason 区分 'yaml-unavailable' / 'compose-failed'),由调用方走既有的「只落
510
- * 文件、跳过声明行并 warn、下次自愈」分支。
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、下次自愈」分支。
511
520
  */
512
521
  /**
513
522
  * preset 声明行里的本地插件名换成 file:// 绝对地址。相对名按「声明方 baseUrl」
@@ -540,6 +549,14 @@ export function composeManagedRows(home, logger = console) {
540
549
  const hostVersion = readHostVersion(home)
541
550
  const useV2 = hostVersion !== null && hostRejectsInlineDelegation(hostVersion)
542
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
+ }
543
560
  const yaml = loadYamlLib(home)
544
561
  if (!yaml) return { shape, rows: null, hostVersion, yaml: null, reason: 'yaml-unavailable' }
545
562
  try {
@@ -664,6 +681,39 @@ export function upsertManagedBlock(dshHome, blockText, { versionMarkerCurrent, l
664
681
  return span !== null ? 'updated' : 'created'
665
682
  }
666
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
+
667
717
  // ── 方法论技能落盘 ──────────────────────────────────────────────────────────
668
718
 
669
719
  /** 技能源目录(包内 skills/)。 */
@@ -895,28 +945,37 @@ function json(res, status, payload) {
895
945
  }
896
946
 
897
947
  // 错误原文(可能带本机绝对路径)只进日志;回给浏览器的 message 先把插件
898
- // 安装根与 home 目录替换成 ~ 再说。
948
+ // 安装根与目录锚点替换成 ~ 再说。
899
949
  const SANITIZE_HOME_DIR = path.resolve(os.homedir())
900
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
+ })()
901
958
 
902
959
  function sanitizeMessage(message) {
903
960
  let text = String(message ?? '')
904
- // 先长后短:安装根通常是 home 的子路径,先替换更具体的
961
+ // 先长后短:安装根通常是 home/DSH_HOME 的子路径,先替换更具体的
905
962
  text = text.split(SANITIZE_PLUGIN_ROOT).join('~')
906
963
  text = text.split(SANITIZE_HOME_DIR).join('~')
964
+ if (SANITIZE_DSH_HOME !== null) text = text.split(SANITIZE_DSH_HOME).join('~')
907
965
  return text
908
966
  }
909
967
 
910
968
  /**
911
- * 递归脱敏:结构里所有字符串值过一遍 sanitizeMessage(绝对路径换 ~)。host-debug
969
+ * 递归脱敏:结构里所有字符串值过一遍 sanitize(默认模块级 sanitizeMessage,
970
+ * 可传路由层的 sanitizeWithRoots 把已注册工作区根并入替换表)。host-debug
912
971
  * 响应用——trace.via / runtime.argv / execPath / cwd 等带本机绝对路径的字段,
913
972
  * 与错误 message 同一纪律:诊断信息照给,本机路径不出门。
914
973
  */
915
- function sanitizeDeep(value) {
916
- if (typeof value === 'string') return sanitizeMessage(value)
917
- 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))
918
977
  if (value !== null && typeof value === 'object') {
919
- 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)]))
920
979
  }
921
980
  return value
922
981
  }
@@ -946,7 +1005,7 @@ export async function registerPrefixRouteWithRetry(webServer, route, { attempts
946
1005
  }
947
1006
 
948
1007
  /**
949
- * 注册创作台全部路由(docs/creator-panel.md §2 的 19 条)。返回宿主 disposer;
1008
+ * 注册创作台全部路由(docs/creator-panel.md §2 的 23 条)。返回宿主 disposer;
950
1009
  * webServer 缺席(非 web 部署)时抛错,由 apply 侧统一降级为 warn。
951
1010
  */
952
1011
  export async function registerCreatorRoutes(ctx) {
@@ -960,13 +1019,37 @@ export async function registerCreatorRoutes(ctx) {
960
1019
  const trustedHostsOf = () => {
961
1020
  try { return ctx.get('webRuntime')?.trustedHosts ?? [] } catch { return [] }
962
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
+ }
963
1044
 
964
1045
  return registerPrefixRouteWithRetry(webServer, {
965
1046
  kind: 'prefix',
966
1047
  path: '/api/maoying',
967
1048
  handler: async (req, res) => {
968
1049
  if (!isTrustedApiRequest(req, trustedHostsOf())) {
969
- json(res, 403, { error: 'forbidden' })
1050
+ // 错误形状契约(本文件 §路由要点:「错误形状统一 { ok: false, error }」):
1051
+ // 围栏 403 也带 ok:false,客户端形状检查不必对这一条网开一面。
1052
+ json(res, 403, { ok: false, error: 'forbidden' })
970
1053
  return
971
1054
  }
972
1055
  try {
@@ -987,7 +1070,7 @@ export async function registerCreatorRoutes(ctx) {
987
1070
  res.end(css)
988
1071
  } catch (err) {
989
1072
  ctx.logger?.warn?.('maoying 创作台: client.css 读取失败: ' + String(err?.message ?? err))
990
- json(res, 500, { ok: false, error: sanitizeMessage(`client.css 不可用: ${err?.message ?? err}`) })
1073
+ json(res, 500, { ok: false, error: sanitizeWithRoots(`client.css 不可用: ${err?.message ?? err}`) })
991
1074
  }
992
1075
  return
993
1076
  }
@@ -995,7 +1078,14 @@ export async function registerCreatorRoutes(ctx) {
995
1078
  if (route === 'update-check' && req.method === 'GET') {
996
1079
  // 新版检测(模仿 dsh-paoding):三级源 + 缓存都在 lib/version.mjs,
997
1080
  // 全失败也静默降级——这里只做转发,绝不影响面板主流程。
998
- json(res, 200, { ok: true, ...(await getVersionInfo({ timeoutMs: 3000 })) })
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
+ })
999
1089
  return
1000
1090
  }
1001
1091
 
@@ -1005,7 +1095,7 @@ export async function registerCreatorRoutes(ctx) {
1005
1095
  // 安全边界见 lib/self-update.mjs)。可预期的失败(dev-link /
1006
1096
  // unsupported-layout / profile-missing / no-target / pnpm-missing /
1007
1097
  // 超时 / 退出码非零 nonzero)统一 HTTP 200 + ok:false,kind 给客户端
1008
- // 分流、message 经 sanitizeMessage 脱敏后透出;成功带 restartRequired:
1098
+ // 分流、message 经 sanitizeWithRoots 脱敏后透出;成功带 restartRequired:
1009
1099
  // true——装完的只是磁盘上的新包,运行中的进程仍是旧代码,必须重启
1010
1100
  // DeepSeek 才生效,客户端文案必须照实转述。本地已不落后线上时短路
1011
1101
  // kind: 'up-to-date'(不装 profile)。两个成功路径都顺带全局 CLI:
@@ -1020,7 +1110,7 @@ export async function registerCreatorRoutes(ctx) {
1020
1110
  restartRequired: true, globalCli: r.globalCli ?? null,
1021
1111
  })
1022
1112
  } else {
1023
- json(res, 200, { ok: false, kind: r.kind, error: sanitizeMessage(r.message ?? r.kind) })
1113
+ json(res, 200, { ok: false, kind: r.kind, error: sanitizeWithRoots(r.message ?? r.kind) })
1024
1114
  }
1025
1115
  return
1026
1116
  }
@@ -1058,18 +1148,18 @@ export async function registerCreatorRoutes(ctx) {
1058
1148
  registryDump.presets = jsonSafe(rows)
1059
1149
  }
1060
1150
  } catch (e) { registryDump.error = String(e?.message ?? e) }
1061
- // 出口统一过 sanitizeDeep:trace.via / argv / execPath / cwd 带本机
1062
- // 绝对路径,诊断照给、路径脱敏(与错误 message 同一纪律)。
1151
+ // 出口统一过 sanitizeDeep + 工作区根扩展表:trace.via / argv / execPath /
1152
+ // cwd 带本机绝对路径,诊断照给、路径脱敏(与错误 message 同一纪律)。
1063
1153
  json(res, 200, {
1064
1154
  ok: true,
1065
1155
  hostVersion: dbg.version,
1066
- trace: sanitizeDeep(dbg.trace),
1156
+ trace: sanitizeDeep(dbg.trace, sanitizeWithRoots),
1067
1157
  shape,
1068
1158
  registry: registryDump,
1069
1159
  runtime: {
1070
- argv: sanitizeDeep(process.argv),
1071
- execPath: sanitizeMessage(process.execPath),
1072
- cwd: sanitizeMessage(process.cwd()),
1160
+ argv: sanitizeDeep(process.argv, sanitizeWithRoots),
1161
+ execPath: sanitizeWithRoots(process.execPath),
1162
+ cwd: sanitizeWithRoots(process.cwd()),
1073
1163
  electron: process.versions?.electron ?? null,
1074
1164
  node: process.version,
1075
1165
  },
@@ -1204,6 +1294,43 @@ export async function registerCreatorRoutes(ctx) {
1204
1294
  return
1205
1295
  }
1206
1296
 
1297
+ if (route === 'file-source' && req.method === 'GET') {
1298
+ // 受管文件读(0.1.20「全量可编辑写面」,四条通用路由之一):rel 白
1299
+ // 名单校验 + 结构口径在 panel-core(managedRelCheck / readManagedFile),
1300
+ // 路由只做围栏与分发。hash 为原文指纹——编辑态保存的 base_hash 基线。
1301
+ const novelDir = resolveNovelRoot(url.searchParams.get('path'), ctx.workspaceRegistry)
1302
+ json(res, 200, readManagedFile(novelDir, url.searchParams.get('rel')))
1303
+ return
1304
+ }
1305
+
1306
+ if (route === 'file-save' && req.method === 'POST') {
1307
+ // 受管文件存:整文件覆写 + 保存时结构校验(解析失败/缺围栏 422)+
1308
+ // base_hash 过期 409(人工改动保护,与 project-save 同口径文案)。
1309
+ const body = await readBody(req, res)
1310
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1311
+ json(res, 200, saveManagedFile(novelDir, body.rel, body.text, { baseHash: body.base_hash }))
1312
+ return
1313
+ }
1314
+
1315
+ if (route === 'file-create' && req.method === 'POST') {
1316
+ // 受管文件建:已存在 409(绝不覆盖),结构校验同存(新角色卡/新章
1317
+ // 纲由客户端模板预填,服务端只验形状)。
1318
+ const body = await readBody(req, res)
1319
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1320
+ json(res, 200, createManagedFile(novelDir, body.rel, body.text))
1321
+ return
1322
+ }
1323
+
1324
+ if (route === 'file-delete' && req.method === 'POST') {
1325
+ // 受管文件删:不可逆,服务端不做二次确认(UI 层强制确认弹层);
1326
+ // 白名单最严——账本 yml / md 实体卡 / outlines 之外只有 chapters/
1327
+ // 的删除(正文编辑走 chapter-save,通用存不开放防绕过状态机语义)。
1328
+ const body = await readBody(req, res)
1329
+ const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
1330
+ json(res, 200, deleteManagedFile(novelDir, body.rel))
1331
+ return
1332
+ }
1333
+
1207
1334
  if (route === 'rebuild-index' && req.method === 'POST') {
1208
1335
  const body = await readBody(req, res)
1209
1336
  const novelDir = resolveNovelRoot(body.path, ctx.workspaceRegistry)
@@ -1221,7 +1348,7 @@ export async function registerCreatorRoutes(ctx) {
1221
1348
  if (err?.responded) return
1222
1349
  json(res, Number.isInteger(err?.statusCode) ? err.statusCode : 500, {
1223
1350
  ok: false,
1224
- error: sanitizeMessage(err?.message ?? String(err)),
1351
+ error: sanitizeWithRoots(err?.message ?? String(err)),
1225
1352
  })
1226
1353
  }
1227
1354
  },
@@ -1236,7 +1363,7 @@ export async function registerCreatorRoutes(ctx) {
1236
1363
  * 2) .installed-version 标记(内容与当前版本一致即不重写);
1237
1364
  * 3) 方法论技能落盘到 <home>/skills/maoying-*(不依赖 yaml,独立可失败);
1238
1365
  * 4) home patch 托管块(依赖 yaml;形态随宿主版本,见 composeManagedRows;
1239
- * yaml 缺席或组合源坏时跳过并 warn,下次启动自愈)。
1366
+ * yaml 缺席、组合源坏或宿主版本探测全败时跳过并 warn,下次启动自愈)。
1240
1367
  * 返回 { status, home, presetDir, copied, skillsCopied, blockAction, reason,
1241
1368
  * shape, hostVersion },status 取 'ok'(全链路完成)| 'partial'(托管块未写,
1242
1369
  * 原因见 reason);shape 为本次所选委派行形态('v1' | 'v2');skillsCopied 为
@@ -1287,8 +1414,12 @@ export function ensurePresetInstalled(ctx = {}) {
1287
1414
  composed.reason === 'yaml-unavailable'
1288
1415
  ? 'maoying-installer: <home>/node_modules 下找不到 yaml 包,无法解析预设组合——' +
1289
1416
  '预设文件已复制,但 cordis.patch.yml 声明行本次跳过,下次宿主启动自动补写'
1290
- : 'maoying-installer: 预设组合源解析失败——预设文件已复制,但 cordis.patch.yml 声明行本次跳过,' +
1291
- '修复后下次宿主启动自动补写(详见上方 warn)',
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)',
1292
1423
  )
1293
1424
  return {
1294
1425
  status: 'partial',
@@ -1334,6 +1465,13 @@ export function ensurePresetInstalled(ctx = {}) {
1334
1465
  */
1335
1466
  export function apply(ctx = {}, _config = {}) {
1336
1467
  const logger = ctx?.logger ?? console
1468
+ // 启动顺带清扫上次崩溃可能残留的托管块临时件(见 sweepStaleManagedBlockTmp;
1469
+ // 自吞错、只认本插件命名前缀且 mtime>1h,绝不影响启动)。
1470
+ try {
1471
+ sweepStaleManagedBlockTmp(resolveDshHome())
1472
+ } catch {
1473
+ /* 防御性兜底:函数本身不抛,这里再兜一层 */
1474
+ }
1337
1475
  try {
1338
1476
  ensurePresetInstalled({ logger })
1339
1477
  } catch (error) {
package/lib/base.css CHANGED
@@ -235,6 +235,10 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
235
235
  font-weight: 600;
236
236
  }
237
237
  .myt-subTab .myt-boardCount { line-height: 16px; }
238
+ /* 二级分类 tab(设定 tab 内 world 分区按 category 再切):比一级子 tab 再小一号 */
239
+ .myt-subTabsInner { margin-top: -2px; }
240
+ .myt-subTabsInner .myt-subTab { font-size: 12px; line-height: 16px; padding: 3px 8px; border-radius: 6px; }
241
+ .myt-subTabsInner .myt-subTab .myt-boardCount { line-height: 14px; }
238
242
 
239
243
  /* ── 卡片 / 栅格 ───────────────────────────────────────────────────────── */
240
244
  .myt-card {
@@ -671,6 +675,17 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
671
675
  .myt-modalHead { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px; border-bottom: 1px solid var(--dsw-alias-border-l1); }
672
676
  .myt-modalTitle { margin: 0; min-width: 0; font-size: 14px; line-height: 20px; font-weight: 600; color: var(--dsw-alias-label-primary); overflow-wrap: anywhere; }
673
677
  .myt-modalBody { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 14px 16px; display: flex; flex-direction: column; gap: 10px; }
678
+ /* ── 受管文件写面(0.2.x):源文件编辑/新建/删除确认三弹层 ────────────────
679
+ srcCard 比 modalCard 宽一档:整文件 textarea 是主角,宽度优先给代码文本。 */
680
+ .myt-srcCard { width: min(760px, 100%); }
681
+ .myt-srcRel { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11px; line-height: 16px; color: var(--dsw-alias-label-tertiary); overflow-wrap: anywhere; }
682
+ .myt-srcHint { font-size: 11px; line-height: 16px; }
683
+ .myt-srcArea { min-height: 46vh; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12px; line-height: 1.6; }
684
+ .myt-itemActions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 2px; }
685
+ .myt-itemActions .myt-btn { padding: 2px 8px; font-size: 11px; line-height: 16px; }
686
+ .myt-confirmCard { width: min(460px, 100%); }
687
+ .myt-confirmText { font-size: 12px; line-height: 19px; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; white-space: pre-wrap; }
688
+ .myt-outlineDelBtn { color: var(--dsw-alias-state-error-primary); }
674
689
  .myt-kvRow { display: flex; gap: 10px; font-size: 12px; line-height: 18px; }
675
690
  .myt-kvKey { flex: none; width: 64px; color: var(--dsw-alias-label-tertiary); }
676
691
  .myt-kvVal { min-width: 0; color: var(--dsw-alias-label-secondary); overflow-wrap: anywhere; white-space: pre-wrap; }
@@ -710,6 +725,15 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
710
725
  }
711
726
  .myt-graphFoot { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; color: var(--dsw-alias-label-tertiary); font-size: 11px; }
712
727
 
728
+ /* ── 关系图角色图例:色点 + 角色名(canvas 内色同 NODE_ROLE_COLORS)──── */
729
+ .myt-graphLegend { display: inline-flex; align-items: center; gap: 10px; margin-left: auto; }
730
+ .myt-legendItem { display: inline-flex; align-items: center; gap: 4px; }
731
+ .myt-legendDot { width: 8px; height: 8px; border-radius: 50%; flex: none; }
732
+ .myt-legendDot[data-role="protagonist"] { background: #f53f3f; }
733
+ .myt-legendDot[data-role="supporting"] { background: #165dff; }
734
+ .myt-legendDot[data-role="antagonist"] { background: #1d2129; border: 1px solid var(--dsw-alias-border-l4, var(--dsw-alias-border-l2)); }
735
+ .myt-legendDot[data-role="minor"] { background: #fadc19; border: 1px solid color-mix(in srgb, var(--dsw-alias-label-tertiary) 60%, transparent); }
736
+
713
737
  /* ── 关系图工具角标:+/- 缩放常驻;全屏按钮能力检测通过才渲染 ───────── */
714
738
  .myt-graphTools { position: absolute; top: 10px; right: 10px; display: flex; gap: 6px; z-index: 3; }
715
739
  .myt-graphTool {
@@ -853,8 +877,9 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
853
877
  }
854
878
  .myt-verBadge:hover { background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 22%, transparent); }
855
879
 
856
- /* 一键升级按钮:与版本徽章同形(描边胶囊),装完/安装中禁用降透明度 */
857
- .myt-verUpgrade {
880
+ /* 一键升级 / 手动检测更新按钮:同形(描边胶囊),安装中/检测中禁用降透明度 */
881
+ .myt-verUpgrade,
882
+ .myt-verCheck {
858
883
  margin-left: 6px;
859
884
  padding: 2px 10px;
860
885
  border: 1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 45%, transparent);
@@ -868,8 +893,10 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
868
893
  white-space: nowrap;
869
894
  cursor: pointer;
870
895
  }
871
- .myt-verUpgrade:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent); }
872
- .myt-verUpgrade:disabled { opacity: 0.6; cursor: default; }
896
+ .myt-verUpgrade:hover:not(:disabled),
897
+ .myt-verCheck:hover:not(:disabled) { background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 12%, transparent); }
898
+ .myt-verUpgrade:disabled,
899
+ .myt-verCheck:disabled { opacity: 0.6; cursor: default; }
873
900
 
874
901
  /* ── 版本号 + 系统设置(字号五档,面板整体 zoom)────────────────────────── */
875
902
  .myt-verTag {
@@ -989,6 +1016,15 @@ html[data-dsh-maoying-active]:not([data-dsh-mnemon-active]):not([data-dsh-taskbo
989
1016
  .myt-md a { color: var(--dsw-alias-state-business-primary); text-decoration: none; }
990
1017
  .myt-md a:hover { text-decoration: underline; }
991
1018
  .myt-md strong { color: var(--dsw-alias-label-primary); }
1019
+ .myt-md .myt-mdTableWrap { overflow-x: auto; margin: 6px 0 10px; }
1020
+ /* white-space: normal 必须有:落点容器(.myt-outlineBody / .myt-worldDesc)是
1021
+ pre-wrap,不覆盖则单元格内容按源码换行。 */
1022
+ .myt-md table.myt-mdTable { border-collapse: collapse; width: 100%; white-space: normal; font-size: 12px; line-height: 1.6; }
1023
+ .myt-md .myt-mdTable th, .myt-md .myt-mdTable td { border: 1px solid var(--dsw-alias-border-l1); padding: 5px 9px; text-align: left; vertical-align: top; }
1024
+ .myt-md .myt-mdTable th { background: var(--dsw-alias-bg-layer-3); color: var(--dsw-alias-label-primary); font-weight: 600; }
1025
+ .myt-md .myt-mdTable .myt-ta-l { text-align: left; }
1026
+ .myt-md .myt-mdTable .myt-ta-c { text-align: center; }
1027
+ .myt-md .myt-mdTable .myt-ta-r { text-align: right; }
992
1028
 
993
1029
  /* ── 书稿元信息表单(概览「编辑」态展开)────────────────────────────────── */
994
1030
  .myt-setGrid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px 16px; }