dsh-muv-table 0.3.0 → 0.3.1
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/CHANGELOG.md +38 -1
- package/lib/muv-parser.js +70 -3
- package/lib/panel.html +7 -5
- package/package.json +1 -1
- package/test-muv-parser.mjs +121 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## v0.3.1 (2026-10-12)
|
|
4
|
+
|
|
5
|
+
### 🐛 修复:世界书 `[initvar]` 条目里装的是 **JSON**(不是 YAML)时,整棵变量树读不出来
|
|
6
|
+
|
|
7
|
+
用户口径:**「这个黄金庭院的卡的状态栏老是这样基本上是空白」**(状态栏三项恒显示「未知」,角色网格一个角色都没有)
|
|
8
|
+
|
|
9
|
+
- 现象离病因很远。那张卡自带的状态栏取值是 `_.get(sys, '日期', ['未知'])[0]`,「未知」是模板的
|
|
10
|
+
兜底值 —— 说明脚本**跑到了**,只是喂给它的变量树是空的。
|
|
11
|
+
- 真因:`黄金庭院-昔涟` 把整棵树(`系统.日期/时间/地点` + 全体角色档案)按 MUV 的 `$meta` 约定
|
|
12
|
+
写在一个 `[InitVar]` 世界书条目里,**内容是从 `{` 开始的 JSON**。这条路此前只认三种形态
|
|
13
|
+
(`<initvar>` 标签 / `<VariableInsert>` / 裸 **YAML**),JSON 在最后那条兜底上被
|
|
14
|
+
`looksLikeInitvarMapping` 判「不像变量映射」否决(`{` / `},` / `}` 不匹配 `键: 值`),
|
|
15
|
+
于是 `schemaSource = 'none'`、`initvarData = {}`。(同样不能靠喂给 YAML 解析器救:
|
|
16
|
+
那会儿只得到 `"系统": "{"` 这种垃圾键。)
|
|
17
|
+
- 修法:新增 `parseJsonInitvarMapping()`,在 YAML 闸门**之前**先试 JSON —— 顺序必须如此,
|
|
18
|
+
反过来 JSON 永远进不来(两种形态互斥,不存在抢数据的问题)。认可是**结构性**的:
|
|
19
|
+
`JSON.parse` 成功且结果是纯对象才采用,散文 / 半截 JSON / 顶层数组统统退回 `none`,
|
|
20
|
+
所以"带大括号的说明文档"混不进来。
|
|
21
|
+
- **`$meta` 被剔除**:它是 MVU 对变量树的控制元数据、不是变量 —— 留着会在面板里多出两个假字段
|
|
22
|
+
(`extensible` 读成字符串 `"false"`),还会跟着写进生成的块。
|
|
23
|
+
- 叶子保留 `[值, 说明]` 的**数组**形态(状态栏取 `[0]`,这是 MVU 的写法)。
|
|
24
|
+
- 新来源标 `schemaSource: 'worldbook-initvar-json'`,`panel.html` 的徽标同步加了标签 —— 它和
|
|
25
|
+
`worldbook-initvar` 一样是**启发式**来源,必须可见,否则"猜错"和"这卡确实没变量"长得一样。
|
|
26
|
+
|
|
27
|
+
### ✅ 测试:`test-muv-parser.mjs` [12] 节
|
|
28
|
+
|
|
29
|
+
先量它有多能咬:**换上修复前的解析器跑同一份测试 → 10 条红**,含真实卡那条
|
|
30
|
+
(「黄金庭院:不再读成 none」→ `source=none`);修好后全绿(`80 通过 / 1 失败`,那 1 条是本机
|
|
31
|
+
REAL 卡样本不足导致的**既有红**,改动前后读数一致)。除了分支覆盖,还钉了三件容易空转的事:
|
|
32
|
+
|
|
33
|
+
- **`$meta` 断言先钉反例**:先断言源 JSON 里**确实有** `$meta`,再看它没进结果 —— 否则在树本来就空时这条恒真。
|
|
34
|
+
- **卡片是不可信输入**:JSON 里的 `__proto__` 会被当普通键解析出来,若写成 `vars[key] =` 赋值会真的改写对象原型。突变回朴素赋值后该断言实测变红(`polluted=1`);把剔 `$meta` 那行删掉则 4 条红,现场可见 `$meta.extensible` 混进 schema。
|
|
35
|
+
- **UI 契约**:断言 `panel.html` 的 `SOURCE_LABEL` 覆盖 `schemaSource` 的每个取值 —— 漏一个是界面上渲染出 `undefined`,而不是报错。
|
|
36
|
+
|
|
37
|
+
## v0.3.0 (2026-09-25) —— 补记
|
|
38
|
+
|
|
39
|
+
> 本节正文的代码实际已随 v0.3.0 发布(已核对 npm 上 0.3.0 的 tarball,`lib/client.js` 含 `card-ops-greeting-start`),
|
|
40
|
+
> 但当时 CHANGELOG 一直挂在「未发布」标题下没定型,现在移出并补记到本节。
|
|
4
41
|
|
|
5
42
|
### ✨ 新增:面板内「🧰 卡运维」区块(开场白注入 + 卡与脚本状态)
|
|
6
43
|
|
package/lib/muv-parser.js
CHANGED
|
@@ -127,6 +127,58 @@ function looksLikeInitvarMapping(text) {
|
|
|
127
127
|
return keys > 0
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
/**
|
|
131
|
+
* A `[initvar]` world-book entry whose content is **JSON** rather than bare YAML.
|
|
132
|
+
*
|
|
133
|
+
* `黄金庭院-昔涟` keeps its entire tree that way, and it is not exotic — it is what
|
|
134
|
+
* MUV's own `$meta` convention looks like when written out:
|
|
135
|
+
*
|
|
136
|
+
* {
|
|
137
|
+
* "$meta": { "extensible": false, "strictSet": true },
|
|
138
|
+
* "系统": { "日期": ["2025年04月10日", "格式为YYYY年MM月DD日"], … },
|
|
139
|
+
* "昔涟": { "好感度": ["50", "0-100"], … }
|
|
140
|
+
* }
|
|
141
|
+
*
|
|
142
|
+
* This cannot go through `parseInitvar`: that is a YAML parser, so JSON comes back
|
|
143
|
+
* as junk keys (`"系统"` reads as the string `"{"`), and "it parsed" was never
|
|
144
|
+
* evidence to begin with. JSON is instead recognised **structurally** — `JSON.parse`
|
|
145
|
+
* must succeed *and* yield a plain object. Prose, a broken brace, or a top-level
|
|
146
|
+
* array are all rejected, so a `[initvar]`-marked documentation entry that merely
|
|
147
|
+
* quotes some braces cannot sneak in as a variable tree.
|
|
148
|
+
*
|
|
149
|
+
* `$meta` is MVU's control metadata for the tree, not a variable: left in, it would
|
|
150
|
+
* surface in the panel as two bogus fields (`extensible` reading as the string
|
|
151
|
+
* "false") and round-trip into any block generated from it. Dropped here — nothing
|
|
152
|
+
* reads it today. List values are kept **as lists**, because a leaf is written
|
|
153
|
+
* `[值, 说明]` and the renderer takes `[0]`.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} text - the raw world-book entry content
|
|
156
|
+
* @returns {object|null} the variable tree, or null when this is not JSON data
|
|
157
|
+
*/
|
|
158
|
+
function parseJsonInitvarMapping(text) {
|
|
159
|
+
const trimmed = String(text ?? '').trim()
|
|
160
|
+
if (!trimmed.startsWith('{')) return null
|
|
161
|
+
|
|
162
|
+
let parsed
|
|
163
|
+
try { parsed = JSON.parse(trimmed) } catch (_) { return null }
|
|
164
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return null
|
|
165
|
+
|
|
166
|
+
const vars = {}
|
|
167
|
+
let kept = 0
|
|
168
|
+
for (const key of Object.keys(parsed)) {
|
|
169
|
+
if (key === '$meta') continue
|
|
170
|
+
// defineProperty rather than `vars[key] =`: JSON.parse accepts `__proto__` as
|
|
171
|
+
// an ordinary key, and assigning it would genuinely rewrite this object's
|
|
172
|
+
// prototype. Card content is untrusted input.
|
|
173
|
+
Object.defineProperty(vars, key, {
|
|
174
|
+
value: parsed[key], enumerable: true, writable: true, configurable: true
|
|
175
|
+
})
|
|
176
|
+
kept++
|
|
177
|
+
}
|
|
178
|
+
// A tree made only of `$meta` declares no variables; that stays a rejection.
|
|
179
|
+
return kept > 0 ? vars : null
|
|
180
|
+
}
|
|
181
|
+
|
|
130
182
|
/** The first `<initvar>` block in a list of texts, or null. */
|
|
131
183
|
function firstInitvarBlock(texts) {
|
|
132
184
|
for (const text of texts) {
|
|
@@ -205,9 +257,10 @@ export function extractVariableInsert(data) {
|
|
|
205
257
|
* Parse a MUV character card JSON and extract schema + variable data.
|
|
206
258
|
* @param {object} cardJson - The full character card JSON
|
|
207
259
|
* @returns {{ name: string, schemas: object[], initvarData: object, zodSource: string,
|
|
208
|
-
* schemaSource: 'initvar'|'variable-insert'|'worldbook-initvar'|'none' }}
|
|
260
|
+
* schemaSource: 'initvar'|'variable-insert'|'worldbook-initvar'|'worldbook-initvar-json'|'none' }}
|
|
209
261
|
* `schemaSource` names the source the variable tree was read from ('none' when the
|
|
210
262
|
* card carries none), so a reader can tell a real tree from the heuristic fallback.
|
|
263
|
+
* '-json' means the `[initvar]` world-book entry held JSON instead of bare YAML.
|
|
211
264
|
*/
|
|
212
265
|
export function parseMuvCard(cardJson) {
|
|
213
266
|
const data = cardData(cardJson)
|
|
@@ -281,8 +334,11 @@ export function parseMuvCard(cardJson) {
|
|
|
281
334
|
}
|
|
282
335
|
}
|
|
283
336
|
|
|
284
|
-
// Last of the last resorts: a world-book entry marked `[initvar]…` whose content
|
|
285
|
-
//
|
|
337
|
+
// Last of the last resorts: a world-book entry marked `[initvar]…` whose content has
|
|
338
|
+
// no tag at all (see worldbookInitvarEntries). Two content shapes are recognised here:
|
|
339
|
+
// bare **YAML** — every card this fallback was built for — and **JSON** written with
|
|
340
|
+
// MUV's `$meta` convention (`黄金庭院-昔涟`; see parseJsonInitvarMapping). Both answer
|
|
341
|
+
// to the same four gates:
|
|
286
342
|
//
|
|
287
343
|
// ① only after every source above came up empty — a card that already parses from
|
|
288
344
|
// a greeting must not change where it reads from (native `<initvar>` wins);
|
|
@@ -302,6 +358,17 @@ export function parseMuvCard(cardJson) {
|
|
|
302
358
|
// marker plus gate ② — a disabled entry *without* that marker is never read.
|
|
303
359
|
if (initvarRaw.length === 0) {
|
|
304
360
|
for (const entry of worldbookInitvarEntries(data)) {
|
|
361
|
+
// JSON first — order matters. Seen through the YAML gate below, a JSON tree is
|
|
362
|
+
// just rows that do not read as `键: 值` (`{`, `},`, `}` …), so
|
|
363
|
+
// looksLikeInitvarMapping rejects it outright and the JSON branch would never
|
|
364
|
+
// run. The two shapes are mutually exclusive, never in competition for data.
|
|
365
|
+
const asJson = parseJsonInitvarMapping(entry.content)
|
|
366
|
+
if (asJson) {
|
|
367
|
+
initvarData = asJson
|
|
368
|
+
initvarRaw = [String(entry.content).trim()]
|
|
369
|
+
schemaSource = 'worldbook-initvar-json'
|
|
370
|
+
break
|
|
371
|
+
}
|
|
305
372
|
if (!looksLikeInitvarMapping(entry.content)) continue
|
|
306
373
|
const parsed = parseInitvar(entry.content)
|
|
307
374
|
if (!parsed || typeof parsed !== 'object' || Object.keys(parsed).length === 0) continue
|
package/lib/panel.html
CHANGED
|
@@ -46,11 +46,13 @@ body { font-family: 'Segoe UI', 'Noto Sans SC', system-ui, sans-serif; backgroun
|
|
|
46
46
|
</div>
|
|
47
47
|
<script>
|
|
48
48
|
let state = { cardName: '', schemas: [], initvarData: {}, edits: {}, generatedBlock: '' }
|
|
49
|
-
// Where the variable tree was read from, shown next to the card name.
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// card that genuinely declares its
|
|
53
|
-
|
|
49
|
+
// Where the variable tree was read from, shown next to the card name. The two
|
|
50
|
+
// `worldbook-initvar*` entries are *heuristic* sources (a world-book entry marked
|
|
51
|
+
// `[initvar]`, no tag, holding bare YAML or JSON), so they have to be visible —
|
|
52
|
+
// otherwise a wrong guess looks exactly like a card that genuinely declares its
|
|
53
|
+
// variables. Every value `parseMuvCard` can emit must have a label here; a missing
|
|
54
|
+
// one would render "undefined" instead of failing loudly (checked in test-muv-parser).
|
|
55
|
+
const SOURCE_LABEL = { 'initvar': '<initvar> 块', 'variable-insert': '<VariableInsert>', 'worldbook-initvar': '世界书 [initvar] 条目', 'worldbook-initvar-json': '世界书 [initvar] 条目 (JSON)', 'none': '' }
|
|
54
56
|
function badgeText(d) {
|
|
55
57
|
const label = SOURCE_LABEL[d.schemaSource]
|
|
56
58
|
return d.name + (label ? ' · ' + label : '')
|
package/package.json
CHANGED
package/test-muv-parser.mjs
CHANGED
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
// ⑥ `键: ""` 空字符串被序列化成裸的 `键: `,再解析回来变成 `{}`(类型静默翻转)
|
|
11
11
|
// ⑦ 世界书里只有注释前缀 `[initvar]`、没有 `<initvar>` 标签的裸 YAML 条目读不到
|
|
12
12
|
// → 该卡 schemas 永远是 0(`异世界农场`);[11] 节连四道闸一起钉住
|
|
13
|
+
// ⑧ 同一个 `[initvar]` 条目装的是 **JSON** 而不是 YAML(`黄金庭院-昔涟`)→ 整棵变量树
|
|
14
|
+
// 读不出来,状态栏 `_.get(sys,'日期',['未知'])[0]` 恒走兜底;[12] 节
|
|
13
15
|
//
|
|
14
16
|
// 真实卡只在存在时才测(缺失 → SKIP),合成卡负责精确覆盖每条分支。
|
|
15
17
|
|
|
@@ -484,5 +486,124 @@ check('★ disabled 且没有 [initvar] 标记的条目永远不读',
|
|
|
484
486
|
data: { name: 'dis2', first_mes: '没变量', character_book: { entries: [{ comment: '变量初始化', content: '甲: 1', enabled: false }] } },
|
|
485
487
|
}).schemaSource === 'none')
|
|
486
488
|
|
|
489
|
+
// ─────────────────────────────────────────────────────────
|
|
490
|
+
// ⑧ 世界书 `[initvar]` 条目里装的是 **JSON** 而不是 YAML(`黄金庭院-昔涟` 就是这种)。
|
|
491
|
+
//
|
|
492
|
+
// 病灶现象在 UI 上很远:卡自带的状态栏恒为「未知」、角色网格一个角色都没有。真因是
|
|
493
|
+
// 那张卡把整棵变量树(`系统.日期/时间/地点` + 全体角色档案)以 MUV 的 `$meta` 约定
|
|
494
|
+
// 写在一个 `[InitVar]` 世界书条目里,内容是从 `{` 开始的 JSON;走到 YAML 闸门时被判
|
|
495
|
+
// 「不像变量映射」否决(`{` / `},` / `}` 不是 `键: 值`),于是 schemaSource 落到
|
|
496
|
+
// 'none'、`initvarData` 是空对象 —— 状态栏的 `_.get(sys,'日期',['未知'])[0]` 永远只能
|
|
497
|
+
// 拿到兜底值,跟运行时有没有变量无关。
|
|
498
|
+
console.log('\n[12] 世界书 [initvar] 条目装的是 JSON(黄金庭院-昔涟)')
|
|
499
|
+
|
|
500
|
+
// 形状照真实卡:$meta 是 MVU 的控制元数据;叶子写成 [值, 说明] 的数组。
|
|
501
|
+
const JSON_TREE = JSON.stringify({
|
|
502
|
+
'$meta': { extensible: false, strictSet: true },
|
|
503
|
+
'系统': {
|
|
504
|
+
'日期': ['2025年04月10日', '格式为YYYY年MM月DD日'],
|
|
505
|
+
'时间': ['09:00', '格式为hh:mm'],
|
|
506
|
+
'地点': ['黄金庭院-客厅', '当前主要场景地点'],
|
|
507
|
+
},
|
|
508
|
+
'昔涟': { '好感度': ['50', '0-100'] },
|
|
509
|
+
}, null, 2)
|
|
510
|
+
|
|
511
|
+
/** 一张世界书装了给定内容的合成卡;first_mes 留给「更高优先级来源」的用例用。 */
|
|
512
|
+
function cardWithWbJson(content, firstMes = '没变量') {
|
|
513
|
+
return parseMuvCard({
|
|
514
|
+
data: {
|
|
515
|
+
name: 'wbjson',
|
|
516
|
+
first_mes: firstMes,
|
|
517
|
+
// enabled:false 是作者的约定(把变量树关掉免得注入上下文),必须照样能读。
|
|
518
|
+
character_book: { entries: [{ comment: '[InitVar]', content, enabled: false }] },
|
|
519
|
+
},
|
|
520
|
+
})
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const j1 = cardWithWbJson(JSON_TREE)
|
|
524
|
+
check('★ JSON 变量树被读出来源 = worldbook-initvar-json',
|
|
525
|
+
j1.schemaSource === 'worldbook-initvar-json', 'source=' + j1.schemaSource)
|
|
526
|
+
check('系统.日期/时间/地点 都在,且保留 [值, 说明] 数组形态',
|
|
527
|
+
at(j1.initvarData, '系统.日期')?.[0] === '2025年04月10日' &&
|
|
528
|
+
at(j1.initvarData, '系统.时间')?.[0] === '09:00' &&
|
|
529
|
+
at(j1.initvarData, '系统.地点')?.[0] === '黄金庭院-客厅',
|
|
530
|
+
JSON.stringify(at(j1.initvarData, '系统')))
|
|
531
|
+
check('★ 状态栏口径不再走兜底:`(_.get(sys,"日期")||["未知"])[0]`',
|
|
532
|
+
(at(j1.initvarData, '系统.日期') || ['未知'])[0] === '2025年04月10日',
|
|
533
|
+
'取到=' + JSON.stringify((at(j1.initvarData, '系统.日期') || ['未知'])[0]))
|
|
534
|
+
// 「$meta 不在结果里」这种断言空转时会恒真(树本来就是空的也说没有 $meta),所以
|
|
535
|
+
// 先把反例钉住:源 JSON **确实带** $meta(这里也兜住 JSON_TREE 本身被改坏的情况),
|
|
536
|
+
// 再断言它没进结果 —— 这样它量的是「被筛掉」,而不是「压根没喂进来」。
|
|
537
|
+
check('★ 源 JSON 里确实有 $meta,且没进变量树 / schema',
|
|
538
|
+
Object.keys(JSON.parse(JSON_TREE)).includes('$meta') &&
|
|
539
|
+
!Object.keys(j1.initvarData).includes('$meta') && !j1.schemas.some(s => s.name === '$meta'),
|
|
540
|
+
'源 keys=' + Object.keys(JSON.parse(JSON_TREE)).join(',') + ' | 结果 keys=' + Object.keys(j1.initvarData).join(','))
|
|
541
|
+
check('schema 建起来了(系统 + 昔涟)',
|
|
542
|
+
j1.schemas.length === 2 && fieldAt(j1.schemas, '系统.日期') !== undefined,
|
|
543
|
+
'schemas=' + j1.schemas.length + ' leaves=' + leafPaths(j1.schemas).join(','))
|
|
544
|
+
|
|
545
|
+
// 闸① 这条兜底只在其它来源全空时启用 —— 能解析的卡不许换数据源
|
|
546
|
+
check('闸① 问候语里的 <initvar> 优先于 JSON 世界书条目',
|
|
547
|
+
cardWithWbJson(JSON_TREE, '<initvar>\n名字: 问候语里的\n</initvar>').schemaSource === 'initvar')
|
|
548
|
+
check('闸① <VariableInsert> 也优先于 JSON 世界书条目',
|
|
549
|
+
cardWithWbJson(JSON_TREE, '<VariableInsert>{"问候语里的":1}</VariableInsert>').schemaSource === 'variable-insert')
|
|
550
|
+
|
|
551
|
+
// 闸② 形状判定:必须**真的读到 JSON 数据**才采用。凡是进不了 JSON.parse 或不是对象的,
|
|
552
|
+
// 一律退回 none —— 带着半句话就说 { 的文档条目不能混进来。
|
|
553
|
+
const yamlCase = 'YAML 照旧走 YAML 分支'
|
|
554
|
+
for (const [label, content] of [
|
|
555
|
+
['破碎的 JSON(括号没闭合)', '{\n "系统": {\n "日期": ["x"]\n'],
|
|
556
|
+
['顶层是数组的 JSON', '[{"系统":{"日期":["x"]}}]'],
|
|
557
|
+
['只有 $meta 的 JSON(等于没变量)', '{"$meta":{"extensible":false}}'],
|
|
558
|
+
['以 { 开头但不是 JSON 的散文', '{ 变量初始化格式说明:请在回复结尾输出 JSONPatch }'],
|
|
559
|
+
['null / 标量 JSON', '123'],
|
|
560
|
+
[yamlCase, '甲: 1\n乙:\n 丙: 2'],
|
|
561
|
+
]) {
|
|
562
|
+
const r = cardWithWbJson(content)
|
|
563
|
+
const ok = label === yamlCase
|
|
564
|
+
? (r.schemaSource === 'worldbook-initvar' && at(r.initvarData, '乙.丙') === 2)
|
|
565
|
+
: (r.schemaSource === 'none' && Object.keys(r.initvarData).length === 0)
|
|
566
|
+
check('闸② ' + label + ' -> ' + (label === yamlCase ? 'worldbook-initvar' : '不采用'),
|
|
567
|
+
ok, 'source=' + r.schemaSource + ' data=' + JSON.stringify(r.initvarData))
|
|
568
|
+
}
|
|
569
|
+
check('前后有 BOM/空白的 JSON 仍读得出(判定用 trim 后的内容)',
|
|
570
|
+
cardWithWbJson('\n\n ' + JSON_TREE + '\n\n').schemaSource === 'worldbook-initvar-json')
|
|
571
|
+
|
|
572
|
+
// ★ 卡片内容是**不可信输入**:JSON.parse 允许 `__proto__` 当普通键,若解析用
|
|
573
|
+
// `vars[key] = value` 赋值,这条就会真的改写对象原型。这条断言在赋值写法下必红。
|
|
574
|
+
const j2 = cardWithWbJson('{"__proto__":{"polluted":1},"系统":{"日期":["2025年04月10日"]}}')
|
|
575
|
+
check('★ JSON 里的 __proto__ 只是普通键,不改写原型',
|
|
576
|
+
Object.getPrototypeOf(j2.initvarData) === Object.prototype && j2.initvarData.polluted === undefined &&
|
|
577
|
+
at(j2.initvarData, '系统.日期')?.[0] === '2025年04月10日',
|
|
578
|
+
'proto改了吗=' + (Object.getPrototypeOf(j2.initvarData) !== Object.prototype) +
|
|
579
|
+
' polluted=' + j2.initvarData.polluted + ' data=' + JSON.stringify(j2.initvarData))
|
|
580
|
+
|
|
581
|
+
// ★ UI 契约:schemaSource 的每个取值都要在 panel.html 的 SOURCE_LABEL 里有对应标签,
|
|
582
|
+
// 漏一个就是界面上渲染出 "undefined"(顺便:这也让「返回值的取值域」有了权威清单)。
|
|
583
|
+
const panelHtml = fs.readFileSync(new URL('./lib/panel.html', import.meta.url), 'utf8')
|
|
584
|
+
const labelBody = /const SOURCE_LABEL = \{([^}]*)\}/.exec(panelHtml)?.[1] ?? ''
|
|
585
|
+
const labelled = new Set([...labelBody.matchAll(/'([^']*)'\s*:/g)].map(m => m[1]))
|
|
586
|
+
const EMITTED = ['initvar', 'variable-insert', 'worldbook-initvar', 'worldbook-initvar-json', 'none']
|
|
587
|
+
const missingLabel = EMITTED.filter(v => !labelled.has(v))
|
|
588
|
+
check('★ panel.html 的 SOURCE_LABEL 覆盖所有 schemaSource 取值',
|
|
589
|
+
missingLabel.length === 0 && labelled.size > 0,
|
|
590
|
+
'漏: ' + missingLabel.join(',') + ' | 现有标签=' + [...labelled].join(','))
|
|
591
|
+
|
|
592
|
+
// 真实卡:这条断言能在修复前变红 —— 它量的是「病因」本身,不是中间过程。
|
|
593
|
+
const golden = findCard('黄金庭院')
|
|
594
|
+
if (!golden) {
|
|
595
|
+
console.log(' SKIP 找不到 黄金庭院(期望 ~/Downloads/黄金庭院.png)')
|
|
596
|
+
} else {
|
|
597
|
+
const g = parseMuvCard(readPngCard(golden))
|
|
598
|
+
check('★ 真实卡黄金庭院:不再读成 none',
|
|
599
|
+
g.schemaSource === 'worldbook-initvar-json', 'source=' + g.schemaSource)
|
|
600
|
+
check('状态栏三个字段都拿到真实值',
|
|
601
|
+
['日期', '时间', '地点'].every(k => String(at(g.initvarData, '系统.' + k)?.[0] || '').length > 0),
|
|
602
|
+
JSON.stringify(at(g.initvarData, '系统')))
|
|
603
|
+
check('角色档案进了变量树(昔涟)',
|
|
604
|
+
at(g.initvarData, '昔涟.好感度') !== undefined, 'keys=' + Object.keys(g.initvarData).slice(0, 8).join(','))
|
|
605
|
+
check('$meta 没混进 schema', !g.schemas.some(s => s.name === '$meta'))
|
|
606
|
+
}
|
|
607
|
+
|
|
487
608
|
console.log(`\n=== 结果: ${pass} 通过, ${fail} 失败 ===`)
|
|
488
609
|
process.exit(fail ? 1 : 0)
|