dsh-muv-table 0.2.10 → 0.2.11
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 +53 -15
- package/lib/index.js +6 -4
- package/lib/muv-parser.js +105 -2
- package/lib/panel.html +10 -1
- package/package.json +1 -1
- package/test-muv-parser.mjs +127 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## v0.2.
|
|
3
|
+
## v0.2.11 (2026-09-20)
|
|
4
|
+
### 🐛 修复:世界书里「没有 `<initvar>` 标签、只有 `[initvar]` 注释前缀」的裸 YAML 变量树读不到
|
|
5
|
+
|
|
6
|
+
**现象**:`异世界农场` 的整棵变量树**不在卡文本里**,而在世界书条目
|
|
7
|
+
`[initvar]变量初始化勿开` 的 `content` 里,是裸 YAML:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
时间:
|
|
11
|
+
日期: '05-20'
|
|
12
|
+
星期: 周日
|
|
13
|
+
种族好感度:
|
|
14
|
+
凛原族: 0
|
|
15
|
+
…
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
它**没有 `<initvar>` 标签**,所以按标签扫描的候选集
|
|
19
|
+
(`first_mes` / `description` / `scenario` / `alternate_greetings` / 世界书 / helper)
|
|
20
|
+
永远扫不到 → 该卡 `schemas` 恒为 `0`,面板一片空白。
|
|
21
|
+
|
|
22
|
+
**修法**(`lib/muv-parser.js`):新增 `worldbookInitvarEntries()`(找 `comment` 以
|
|
23
|
+
`[initvar]` 开头的世界书条目)与 `looksLikeInitvarMapping()`(content 必须**读起来像
|
|
24
|
+
变量映射**:`键:` / `键: 值` 行算键、`- item` 算列表内容、空行与 `#` 注释跳过,
|
|
25
|
+
**只要有一行不属于这三类就否决**)。四道闸:① 只在上面所有来源都落空时启用;
|
|
26
|
+
② content 必须过形状判定;③ 解析出的映射非空;④ 响应里标
|
|
27
|
+
`schemaSource: 'worldbook-initvar'`,来源可见(面板上显示在卡名后面)。
|
|
28
|
+
|
|
29
|
+
**★ 为什么它会读一条 `enabled: false` 的条目**(这条请照原样保留,否则会被当 bug 报回来):
|
|
30
|
+
要读的那条条目**本身就是被禁用的**,而且 `comment` 里就写着「变量初始化**勿开**」。
|
|
31
|
+
这是卡作者的**约定**:把变量树放进一条禁用条目 → 它不会被注入上下文,但工具链仍然
|
|
32
|
+
可以读到(「勿开」正是这个意思的字面声明)。所以判定只认 `[initvar]` 这个**显式标记**,
|
|
33
|
+
`enabled` 不参与;没有该标记的禁用条目**永远不读**。挡住垃圾的是「标记 + 形状判定」,
|
|
34
|
+
不是 `enabled`。若要求条目必须启用,这条兜底在唯一需要它的卡上就是空转。
|
|
35
|
+
|
|
36
|
+
**实测**:`异世界农场` schemas **0 → 3**(时间 / 种族好感度 / 个人好感度),
|
|
37
|
+
`schemaSource = worldbook-initvar`;`苍玄界`(对应条目叫 `[mvu_update]变量输出格式`,
|
|
38
|
+
是**文档**不是数据)、`_足控天堂2`、`食人世界`、`涩涩提瓦特`、`default_Seraphina`
|
|
39
|
+
**逐字节无回归**;`test-muv-parser.mjs` 56 → 76 项。
|
|
40
|
+
|
|
41
|
+
## v0.2.10 (2026-09-20)
|
|
4
42
|
### 🐛 修复:变量面板读不到「足控天堂2」这种卡的变量表
|
|
5
43
|
|
|
6
44
|
**现象**(已复现):`GET /api/muv-table/tavern-card?presetId=…` 对这张卡返回
|
|
@@ -244,20 +282,20 @@ first_mes, …}`),但 `parseMuvCard` 只认 `cardJson.data` → 卡名对了
|
|
|
244
282
|
但 `[initvar]` 万一也是文档就会解析出垃圾表)。留待决定。现状已用
|
|
245
283
|
`test-muv-parser.mjs` 的第 [10] 节**钉住**(含「没有标签」这条断言),改动必然是有意的。
|
|
246
284
|
|
|
247
|
-
## v0.2.9 (2026-09-20)
|
|
248
|
-
### 🐛 修复:面板「不管点哪个会话都显示同一张卡」
|
|
249
|
-
|
|
250
|
-
服务端无问题(按 `presetId` 请求时每个预设都返回各自的卡)。根因在客户端:它一直发同一个
|
|
251
|
-
预设——因为它依赖酒馆 DOM,而 `#tavern-session-preset-label` 的 `dataset.presetId`
|
|
252
|
-
在切换会话时不一定更新(酒馆自己 `bundle.js:1655` 用 `savedPid || 旧值`,`savedPid`
|
|
253
|
-
为空时**保留旧值**),`localStorage` 那份更是切换预设时才写。**切换会话根本不会更新它们。**
|
|
254
|
-
|
|
255
|
-
改为**优先用当前 DSH 会话 id 请求**:服务端按 `session-bindings.json` 查出该会话绑定的预设,
|
|
256
|
-
切换会话必然跟着变,不会粘。实测 4/5 会话正确解析(第 5 个 `minimal-gitbash` 本身无卡)。
|
|
257
|
-
|
|
258
|
-
注意只传一个定位参数且会话 id 优先——服务端判定顺序是 `if (presetId) … else if (sessionId) …`,
|
|
259
|
-
两个都传时**可能粘住的那个会盖掉可靠的**。
|
|
260
|
-
## v0.2.8 (2026-09-20)
|
|
285
|
+
## v0.2.9 (2026-09-20)
|
|
286
|
+
### 🐛 修复:面板「不管点哪个会话都显示同一张卡」
|
|
287
|
+
|
|
288
|
+
服务端无问题(按 `presetId` 请求时每个预设都返回各自的卡)。根因在客户端:它一直发同一个
|
|
289
|
+
预设——因为它依赖酒馆 DOM,而 `#tavern-session-preset-label` 的 `dataset.presetId`
|
|
290
|
+
在切换会话时不一定更新(酒馆自己 `bundle.js:1655` 用 `savedPid || 旧值`,`savedPid`
|
|
291
|
+
为空时**保留旧值**),`localStorage` 那份更是切换预设时才写。**切换会话根本不会更新它们。**
|
|
292
|
+
|
|
293
|
+
改为**优先用当前 DSH 会话 id 请求**:服务端按 `session-bindings.json` 查出该会话绑定的预设,
|
|
294
|
+
切换会话必然跟着变,不会粘。实测 4/5 会话正确解析(第 5 个 `minimal-gitbash` 本身无卡)。
|
|
295
|
+
|
|
296
|
+
注意只传一个定位参数且会话 id 优先——服务端判定顺序是 `if (presetId) … else if (sessionId) …`,
|
|
297
|
+
两个都传时**可能粘住的那个会盖掉可靠的**。
|
|
298
|
+
## v0.2.8 (2026-09-20)
|
|
261
299
|
### 🐛 修复:Zod「变量结构」整类读不到(覆盖面最大的一个)
|
|
262
300
|
|
|
263
301
|
parseMuvCard() 取 Zod schema 时用的是**精确等值** `s.name === 'zod'`,而真机上的卡
|
package/lib/index.js
CHANGED
|
@@ -440,7 +440,8 @@ function extractMuvFromCharactersJson(presetDir) {
|
|
|
440
440
|
schemas: inferSchemas(clean),
|
|
441
441
|
initvarData: clean,
|
|
442
442
|
initvarBlocks: [body],
|
|
443
|
-
zodSource: ''
|
|
443
|
+
zodSource: '',
|
|
444
|
+
schemaSource: 'variable-insert'
|
|
444
445
|
}
|
|
445
446
|
} catch (_) {
|
|
446
447
|
// Fall through: the block may still be YAML-ish.
|
|
@@ -456,7 +457,8 @@ function extractMuvFromCharactersJson(presetDir) {
|
|
|
456
457
|
schemas: inferSchemas(parsed),
|
|
457
458
|
initvarData: parsed,
|
|
458
459
|
initvarBlocks: [iv[1]],
|
|
459
|
-
zodSource: ''
|
|
460
|
+
zodSource: '',
|
|
461
|
+
schemaSource: 'initvar'
|
|
460
462
|
}
|
|
461
463
|
}
|
|
462
464
|
return null
|
|
@@ -484,10 +486,10 @@ function extractMuvFromYml(yml) {
|
|
|
484
486
|
const blockText = m[1]
|
|
485
487
|
const parsed = parseInitvar(blockText)
|
|
486
488
|
const schemas = inferSchemas(parsed)
|
|
487
|
-
return { name, schemas, initvarData: parsed, initvarBlocks: [blockText], zodSource: '' }
|
|
489
|
+
return { name, schemas, initvarData: parsed, initvarBlocks: [blockText], zodSource: '', schemaSource: 'initvar' }
|
|
488
490
|
}
|
|
489
491
|
// No initvar default — serve empty-but-valid so the panel opens empty
|
|
490
|
-
return { name, schemas: [], initvarData: {}, initvarBlocks: [], zodSource: '' }
|
|
492
|
+
return { name, schemas: [], initvarData: {}, initvarBlocks: [], zodSource: '', schemaSource: 'none' }
|
|
491
493
|
} catch (_) {
|
|
492
494
|
return null
|
|
493
495
|
}
|
package/lib/muv-parser.js
CHANGED
|
@@ -63,6 +63,70 @@ function textsInGroups(candidates, ...groups) {
|
|
|
63
63
|
return candidates.filter(c => groups.includes(c.group)).map(c => c.text)
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* World-book entries that hold a **bare** initvar block — no `<initvar>` tag at all.
|
|
68
|
+
*
|
|
69
|
+
* `异世界农场` keeps its entire variable tree in an entry whose comment is
|
|
70
|
+
* `[initvar]变量初始化勿开` and whose content is plain YAML:
|
|
71
|
+
*
|
|
72
|
+
* 时间:
|
|
73
|
+
* 日期: '05-20'
|
|
74
|
+
* 星期: 周日
|
|
75
|
+
* …
|
|
76
|
+
*
|
|
77
|
+
* Because there is no tag, the tag-based scan can never reach it and the card
|
|
78
|
+
* reported `schemas: 0` even though the whole tree was sitting in the world book.
|
|
79
|
+
*
|
|
80
|
+
* The prefix must be exactly `[initvar]`. The sibling conventions are **documents,
|
|
81
|
+
* not data** and reading them as YAML would build a junk table:
|
|
82
|
+
*
|
|
83
|
+
* - `[mvu_update]变量输出格式` / `[mvu_update]变量更新规则` — how to emit updates
|
|
84
|
+
* (苍玄界 and 异世界农场 both carry these);
|
|
85
|
+
* - `[mvu_plot]…` — illustration prompts.
|
|
86
|
+
*
|
|
87
|
+
* @param {object} data - the card's `data` object
|
|
88
|
+
* @returns {Array<{comment: string, content: string}>}
|
|
89
|
+
*/
|
|
90
|
+
function worldbookInitvarEntries(data) {
|
|
91
|
+
const out = []
|
|
92
|
+
for (const entry of asArray(data?.character_book?.entries)) {
|
|
93
|
+
if (!/^\s*\[initvar\]/i.test(String(entry?.comment || ''))) continue
|
|
94
|
+
if (typeof entry?.content === 'string') out.push({ comment: String(entry.comment), content: entry.content })
|
|
95
|
+
}
|
|
96
|
+
return out
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Does this text read as a YAML **mapping of variables**, rather than prose or a
|
|
101
|
+
* document that happens to mention keys?
|
|
102
|
+
*
|
|
103
|
+
* `parseInitvar` never fails — it drops what it cannot use and returns whatever is
|
|
104
|
+
* left, so "it parsed" is not evidence. This is the actual shape test used to decide
|
|
105
|
+
* whether a `[initvar]`-marked entry is data:
|
|
106
|
+
*
|
|
107
|
+
* - blanks and `# comment` lines are ignored;
|
|
108
|
+
* - `键:` / `键: 值` lines count as keys;
|
|
109
|
+
* - `- item` lines count as list content belonging to the nearest key;
|
|
110
|
+
* - **any other line means this is not a variable mapping** → reject.
|
|
111
|
+
*
|
|
112
|
+
* That last rule is what keeps a prose document out: Chinese prose uses the full-width
|
|
113
|
+
* `:`, so a sentence like `变量初始化格式说明:…` produces no key line at all, and a
|
|
114
|
+
* paragraph next to a stray `键:` line is still rejected (one foreign line is enough).
|
|
115
|
+
* @param {string} text
|
|
116
|
+
* @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function looksLikeInitvarMapping(text) {
|
|
119
|
+
let keys = 0
|
|
120
|
+
for (const rawLine of String(text ?? '').split(/\r?\n/)) {
|
|
121
|
+
const line = rawLine.trim()
|
|
122
|
+
if (!line || line.startsWith('#')) continue
|
|
123
|
+
if (/^-(?:\s|$)/.test(line)) continue
|
|
124
|
+
if (/^[^:\s#][^:]*:(\s|$)/.test(line)) { keys++; continue }
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
return keys > 0
|
|
128
|
+
}
|
|
129
|
+
|
|
66
130
|
/** The first `<initvar>` block in a list of texts, or null. */
|
|
67
131
|
function firstInitvarBlock(texts) {
|
|
68
132
|
for (const text of texts) {
|
|
@@ -140,7 +204,10 @@ export function extractVariableInsert(data) {
|
|
|
140
204
|
/**
|
|
141
205
|
* Parse a MUV character card JSON and extract schema + variable data.
|
|
142
206
|
* @param {object} cardJson - The full character card JSON
|
|
143
|
-
* @returns {{ name: string, schemas: object[], initvarData: object, zodSource: string
|
|
207
|
+
* @returns {{ name: string, schemas: object[], initvarData: object, zodSource: string,
|
|
208
|
+
* schemaSource: 'initvar'|'variable-insert'|'worldbook-initvar'|'none' }}
|
|
209
|
+
* `schemaSource` names the source the variable tree was read from ('none' when the
|
|
210
|
+
* card carries none), so a reader can tell a real tree from the heuristic fallback.
|
|
144
211
|
*/
|
|
145
212
|
export function parseMuvCard(cardJson) {
|
|
146
213
|
const data = cardData(cardJson)
|
|
@@ -187,6 +254,8 @@ export function parseMuvCard(cardJson) {
|
|
|
187
254
|
// Use the first initvar block as default data
|
|
188
255
|
let initvarData = initvarBlocks.length > 0 ? initvarBlocks[0].parsed : {}
|
|
189
256
|
let initvarRaw = initvarBlocks.map(b => b.raw)
|
|
257
|
+
/** Where the variable tree came from — see `schemaSource` in the return value. */
|
|
258
|
+
let schemaSource = initvarRaw.length > 0 ? 'initvar' : 'none'
|
|
190
259
|
|
|
191
260
|
// No <initvar> anywhere → fall back to the community <VariableInsert> shape.
|
|
192
261
|
if (initvarRaw.length === 0) {
|
|
@@ -194,6 +263,7 @@ export function parseMuvCard(cardJson) {
|
|
|
194
263
|
if (inserted) {
|
|
195
264
|
initvarData = inserted.data
|
|
196
265
|
initvarRaw = [inserted.raw]
|
|
266
|
+
schemaSource = 'variable-insert'
|
|
197
267
|
}
|
|
198
268
|
}
|
|
199
269
|
|
|
@@ -207,6 +277,38 @@ export function parseMuvCard(cardJson) {
|
|
|
207
277
|
if (hit) {
|
|
208
278
|
initvarData = hit.parsed
|
|
209
279
|
initvarRaw = [hit.raw]
|
|
280
|
+
schemaSource = 'initvar'
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Last of the last resorts: a world-book entry marked `[initvar]…` whose content is
|
|
285
|
+
// bare YAML with no tag at all (see worldbookInitvarEntries). Four gates:
|
|
286
|
+
//
|
|
287
|
+
// ① only after every source above came up empty — a card that already parses from
|
|
288
|
+
// a greeting must not change where it reads from (native `<initvar>` wins);
|
|
289
|
+
// ② the content must actually read as a YAML mapping (looksLikeInitvarMapping),
|
|
290
|
+
// not as prose/documentation;
|
|
291
|
+
// ③ the caller is told where the tree came from via `schemaSource`, so this
|
|
292
|
+
// heuristic is visible in the API response instead of being silent;
|
|
293
|
+
// ④ the parsed mapping must be non-empty.
|
|
294
|
+
//
|
|
295
|
+
// **`enabled` is deliberately not part of the filter.** The entry this fix exists for
|
|
296
|
+
// — 异世界农场's `[initvar]变量初始化勿开` — is itself `enabled: false`, and its
|
|
297
|
+
// comment literally says 「勿开」(do not enable). That is the convention, not an
|
|
298
|
+
// accident: authors keep the variable tree in a *disabled* entry so it is never
|
|
299
|
+
// injected into the prompt, while tooling can still read it. Skipping disabled
|
|
300
|
+
// entries would make this whole fallback a no-op on the only card that needs it
|
|
301
|
+
// (measured: repro-wb-initvar.mjs). What keeps junk out is the exact `[initvar]`
|
|
302
|
+
// marker plus gate ② — a disabled entry *without* that marker is never read.
|
|
303
|
+
if (initvarRaw.length === 0) {
|
|
304
|
+
for (const entry of worldbookInitvarEntries(data)) {
|
|
305
|
+
if (!looksLikeInitvarMapping(entry.content)) continue
|
|
306
|
+
const parsed = parseInitvar(entry.content)
|
|
307
|
+
if (!parsed || typeof parsed !== 'object' || Object.keys(parsed).length === 0) continue
|
|
308
|
+
initvarData = parsed
|
|
309
|
+
initvarRaw = [String(entry.content).trim()]
|
|
310
|
+
schemaSource = 'worldbook-initvar'
|
|
311
|
+
break
|
|
210
312
|
}
|
|
211
313
|
}
|
|
212
314
|
|
|
@@ -218,7 +320,8 @@ export function parseMuvCard(cardJson) {
|
|
|
218
320
|
schemas,
|
|
219
321
|
initvarData,
|
|
220
322
|
initvarBlocks: initvarRaw,
|
|
221
|
-
zodSource
|
|
323
|
+
zodSource,
|
|
324
|
+
schemaSource
|
|
222
325
|
}
|
|
223
326
|
}
|
|
224
327
|
|
package/lib/panel.html
CHANGED
|
@@ -46,6 +46,15 @@ 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. `worldbook-initvar`
|
|
50
|
+
// is the only *heuristic* source (a world-book entry marked `[initvar]` with bare YAML
|
|
51
|
+
// and no tag), so it has to be visible — otherwise a wrong guess looks exactly like a
|
|
52
|
+
// card that genuinely declares its variables.
|
|
53
|
+
const SOURCE_LABEL = { 'initvar': '<initvar> 块', 'variable-insert': '<VariableInsert>', 'worldbook-initvar': '世界书 [initvar] 条目', 'none': '' }
|
|
54
|
+
function badgeText(d) {
|
|
55
|
+
const label = SOURCE_LABEL[d.schemaSource]
|
|
56
|
+
return d.name + (label ? ' · ' + label : '')
|
|
57
|
+
}
|
|
49
58
|
function handleDrop(e) { e.preventDefault(); const f = e.dataTransfer.files[0]; if (f) loadFile(f) }
|
|
50
59
|
function handleFile(e) { const f = e.target.files[0]; if (f) loadFile(f) }
|
|
51
60
|
function loadFile(f) { const r = new FileReader(); r.onload = () => { try { parseCard(JSON.parse(r.result)) } catch(err) { alert('JSON error: '+err.message) } }; r.readAsText(f) }
|
|
@@ -55,7 +64,7 @@ async function parseCard(json) {
|
|
|
55
64
|
const r = await fetch('/api/muv-table/parse-card', { method:'POST', headers:{'content-type':'application/json'}, body:JSON.stringify(json) })
|
|
56
65
|
const d = await r.json(); if (!d.ok) throw new Error(d.error)
|
|
57
66
|
state = { cardName: d.name, schemas: d.schemas, initvarData: d.initvarData, edits: {}, generatedBlock: '' }
|
|
58
|
-
document.getElementById('badge').style.display = 'inline'; document.getElementById('badge').textContent = d
|
|
67
|
+
document.getElementById('badge').style.display = 'inline'; document.getElementById('badge').textContent = badgeText(d)
|
|
59
68
|
document.getElementById('btnGenerate').style.display = 'inline-block'
|
|
60
69
|
renderTable()
|
|
61
70
|
} catch(e) { document.getElementById('main').innerHTML = '<div style="padding:12px;color:var(--danger)">⚠ '+e.message+'</div>' }
|
package/package.json
CHANGED
package/test-muv-parser.mjs
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
// ④ alternate_greetings(或 entries / scripts)是对象时 for...of 抛 TypeError → 端点 500
|
|
9
9
|
// ⑤ 扁平 V1 卡(没有 data 包装)能被 findJsonMatch 找到,却被 parseMuvCard 读成 0 字段
|
|
10
10
|
// ⑥ `键: ""` 空字符串被序列化成裸的 `键: `,再解析回来变成 `{}`(类型静默翻转)
|
|
11
|
+
// ⑦ 世界书里只有注释前缀 `[initvar]`、没有 `<initvar>` 标签的裸 YAML 条目读不到
|
|
12
|
+
// → 该卡 schemas 永远是 0(`异世界农场`);[11] 节连四道闸一起钉住
|
|
11
13
|
//
|
|
12
14
|
// 真实卡只在存在时才测(缺失 → SKIP),合成卡负责精确覆盖每条分支。
|
|
13
15
|
|
|
@@ -343,13 +345,11 @@ if (!fk) {
|
|
|
343
345
|
}
|
|
344
346
|
|
|
345
347
|
// ─────────────────────────────────────────────────────────
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
// 在彻底找不到变量块时把 content 当 initvar 块解析」),那是另一个决定 —— 见 CHANGELOG。
|
|
352
|
-
console.log('\n[10] 真实卡:异世界农场 —— 裸 [initvar] 世界书条目(现状钉住)')
|
|
348
|
+
// 世界书里还有第三种投递方式:**没有 <initvar> 标签**、只有注释前缀 `[initvar]`
|
|
349
|
+
// 的裸 YAML(`异世界农场` 的 `[initvar]变量初始化勿开`,content 里就是
|
|
350
|
+
// `时间:` / `种族好感度:` 这样的裸文本)。按标签扫描永远扫不到它,所以这张卡
|
|
351
|
+
// 以前是 schemas: 0。
|
|
352
|
+
console.log('\n[10] 真实卡:异世界农场 —— 裸 [initvar] 世界书条目(本轮修复)')
|
|
353
353
|
const farm = findCard('异世界农场')
|
|
354
354
|
if (!farm) {
|
|
355
355
|
console.log(' SKIP 找不到 异世界农场')
|
|
@@ -358,15 +358,131 @@ if (!farm) {
|
|
|
358
358
|
const entry = (card.data.character_book?.entries || [])
|
|
359
359
|
.find(e => /^\[initvar\]/i.test(String(e?.comment || '')))
|
|
360
360
|
check('确实存在注释为 [initvar]… 的世界书条目', !!entry, entry?.comment)
|
|
361
|
-
check('
|
|
361
|
+
check('那份 content 里没有 <initvar> 标签(所以按标签扫不到,这就是本轮要接的洞)',
|
|
362
362
|
!!entry && !/<initvar>/i.test(String(entry.content || '')),
|
|
363
363
|
'content 前 40 字: ' + String(entry?.content || '').slice(0, 40))
|
|
364
364
|
const parsed = parseMuvCard(card)
|
|
365
|
-
check('
|
|
366
|
-
check('
|
|
365
|
+
check('★ schemas 由 0 变成 3 组', parsed.schemas.length === 3, 'schemas=' + parsed.schemas.length)
|
|
366
|
+
check('★ 组名是 时间 / 种族好感度 / 个人好感度',
|
|
367
|
+
parsed.schemas.map(s => s.name).join(',') === '时间,种族好感度,个人好感度',
|
|
368
|
+
parsed.schemas.map(s => s.name).join(','))
|
|
369
|
+
check('★ schemaSource 标成 worldbook-initvar', parsed.schemaSource === 'worldbook-initvar', parsed.schemaSource)
|
|
370
|
+
check('★ 数据读得到(时间.日期 含 05-20)', String(parsed.initvarData?.时间?.日期 || '').includes('05-20'),
|
|
371
|
+
JSON.stringify(parsed.initvarData?.时间))
|
|
372
|
+
check('没有产生垃圾 schema(组名里没有 [mvu_update] 那几张文档的键)',
|
|
373
|
+
!parsed.schemas.some(s => /变量输出格式|变量更新规则|rule/.test(s.name)),
|
|
374
|
+
parsed.schemas.map(s => s.name).join(','))
|
|
375
|
+
check('Zod 变量结构读得到(上一轮的修复仍然有效)', String(parsed.zodSource || '').length > 100,
|
|
367
376
|
'zodSource=' + String(parsed.zodSource || '').length)
|
|
368
|
-
check('没有产生垃圾 schema', parsed.schemas.length === 0 && Object.keys(parsed.initvarData || {}).length === 0)
|
|
369
377
|
}
|
|
370
378
|
|
|
379
|
+
// ─────────────────────────────────────────────────────────
|
|
380
|
+
// schemaSource 的取值契约 + 三道闸(合成用例,精确覆盖每条分支)
|
|
381
|
+
console.log('\n[11] schemaSource 与裸 [initvar] 的三道闸')
|
|
382
|
+
check('没有变量来源时 schemaSource = none',
|
|
383
|
+
parseMuvCard({ data: { name: 'none', first_mes: '没变量' } }).schemaSource === 'none')
|
|
384
|
+
check('<initvar> 标签 -> initvar',
|
|
385
|
+
parseMuvCard({ data: { name: 'a', first_mes: '<initvar>\n甲: 1\n</initvar>' } }).schemaSource === 'initvar')
|
|
386
|
+
check('<VariableInsert> -> variable-insert',
|
|
387
|
+
parseMuvCard({ data: { name: 'b', first_mes: '<VariableInsert>{"甲":1}</VariableInsert>' } }).schemaSource === 'variable-insert')
|
|
388
|
+
|
|
389
|
+
// 闸① 只在其它来源全部落空时启用 —— 能解析的卡不许换数据源
|
|
390
|
+
const g1 = parseMuvCard({
|
|
391
|
+
data: {
|
|
392
|
+
name: 'g1',
|
|
393
|
+
first_mes: '<initvar>\n名字: 问候语里的\n</initvar>',
|
|
394
|
+
character_book: { entries: [{ comment: '[initvar]变量初始化勿开', content: '名字: 世界书里的\n年龄: 3' }] },
|
|
395
|
+
},
|
|
396
|
+
})
|
|
397
|
+
check('闸① 问候语里的 <initvar> 优先于裸 [initvar] 世界书条目',
|
|
398
|
+
at(g1.initvarData, '名字') === '问候语里的' && g1.schemaSource === 'initvar',
|
|
399
|
+
JSON.stringify(g1.initvarData) + ' source=' + g1.schemaSource)
|
|
400
|
+
const g1b = parseMuvCard({
|
|
401
|
+
data: {
|
|
402
|
+
name: 'g1b',
|
|
403
|
+
first_mes: '<VariableInsert>{"问候语里的":1}</VariableInsert>',
|
|
404
|
+
character_book: { entries: [{ comment: '[initvar]变量初始化勿开', content: '名字: 世界书里的' }] },
|
|
405
|
+
},
|
|
406
|
+
})
|
|
407
|
+
check('闸① <VariableInsert> 也优先于裸 [initvar] 世界书条目',
|
|
408
|
+
g1b.initvarData['问候语里的'] === 1 && g1b.schemaSource === 'variable-insert',
|
|
409
|
+
JSON.stringify(g1b.initvarData) + ' source=' + g1b.schemaSource)
|
|
410
|
+
|
|
411
|
+
// 闸② 解析出来是空的就不采用
|
|
412
|
+
for (const [label, content] of [['空串', ''], ['只有空白行', '\n\n \n'], ['只有注释', '# 变量初始化']]) {
|
|
413
|
+
const g2 = parseMuvCard({
|
|
414
|
+
data: { name: 'g2', first_mes: '没变量', character_book: { entries: [{ comment: '[initvar]x', content }] } },
|
|
415
|
+
})
|
|
416
|
+
check('闸② ' + label + ' -> 不采用(schemas=0 且 source=none)',
|
|
417
|
+
g2.schemas.length === 0 && g2.schemaSource === 'none' && Object.keys(g2.initvarData).length === 0,
|
|
418
|
+
'schemas=' + g2.schemas.length + ' source=' + g2.schemaSource + ' data=' + JSON.stringify(g2.initvarData))
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// 前缀边界:兄弟约定是**文档**不是数据(苍玄界 / 异世界农场 都有这几条)
|
|
422
|
+
const docOnly = parseMuvCard({
|
|
423
|
+
data: {
|
|
424
|
+
name: 'doconly',
|
|
425
|
+
first_mes: '没变量',
|
|
426
|
+
character_book: {
|
|
427
|
+
entries: [
|
|
428
|
+
{ comment: '[mvu_update]变量输出格式', content: '---\n变量输出格式:\n rule:\n - must output the update analysis' },
|
|
429
|
+
{ comment: '[mvu_update]变量更新规则', content: '变量更新规则:\n 种族好感度: 0' },
|
|
430
|
+
{ comment: '[mvu_plot]插画强调', content: '---\n[插画强调]\n重要' },
|
|
431
|
+
],
|
|
432
|
+
},
|
|
433
|
+
},
|
|
434
|
+
})
|
|
435
|
+
check('★ [mvu_update] / [mvu_plot] 条目不被当成变量(文档不是数据)',
|
|
436
|
+
docOnly.schemas.length === 0 && docOnly.schemaSource === 'none',
|
|
437
|
+
'schemas=' + docOnly.schemas.length + ' source=' + docOnly.schemaSource + ' data=' + JSON.stringify(docOnly.initvarData))
|
|
438
|
+
|
|
439
|
+
// 闸③ 采用时标出来源
|
|
440
|
+
const g3 = parseMuvCard({
|
|
441
|
+
data: {
|
|
442
|
+
name: 'g3',
|
|
443
|
+
first_mes: '没变量',
|
|
444
|
+
character_book: { entries: [{ comment: '说明: [initvar] 是约定', content: '干扰: 1' }, { comment: '[initvar]变量初始化勿开', content: '甲: 1\n乙:\n 丙: 2' }] },
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
check('闸③ 裸 [initvar] 条目被采用并标出来源(注释里有 [initvar] 但不在行首的不算)',
|
|
448
|
+
g3.schemas.length === 2 && g3.schemaSource === 'worldbook-initvar' && at(g3.initvarData, '乙.丙') === 2,
|
|
449
|
+
'schemas=' + g3.schemas.length + ' source=' + g3.schemaSource + ' data=' + JSON.stringify(g3.initvarData))
|
|
450
|
+
|
|
451
|
+
// 闸②的形状判定:content 必须**读起来像变量映射**。parseInitvar 从不抛异常(它丢掉
|
|
452
|
+
// 用不上的行、返回剩下的),所以"能解析"不能当证据 —— 散文/说明文档必须被挡住。
|
|
453
|
+
for (const [label, content] of [
|
|
454
|
+
['全角冒号的散文', '变量初始化格式说明:请在每轮回复结尾输出 JSONPatch。'],
|
|
455
|
+
['带一个键的说明文档', '说明:\n 本条目用于记录变量初始化格式,具体写法见下方的规则说明文字。'],
|
|
456
|
+
['说明 + 段落(一行不像键就足够否决)', '时间:\n 日期: 05-20\n这一段是作者写给玩家看的说明文字,没有冒号。'],
|
|
457
|
+
]) {
|
|
458
|
+
const g2b = parseMuvCard({
|
|
459
|
+
data: { name: 'g2b', first_mes: '没变量', character_book: { entries: [{ comment: '[initvar]变量初始化勿开', content }] } },
|
|
460
|
+
})
|
|
461
|
+
check('闸② 形状判定挡住:' + label,
|
|
462
|
+
g2b.schemas.length === 0 && g2b.schemaSource === 'none', 'schemas=' + g2b.schemas.length + ' source=' + g2b.schemaSource)
|
|
463
|
+
}
|
|
464
|
+
check('闸② 合法的列表值不会被误否决',
|
|
465
|
+
parseMuvCard({
|
|
466
|
+
data: { name: 'g2c', first_mes: '没变量', character_book: { entries: [{ comment: '[initvar]x', content: '人际交往:\n 沈慕微:\n 最近互动记录:\n - 因为想吃灵鹤被{{user}}抓包。\n 关系标签: 师尊' }] } },
|
|
467
|
+
}).schemaSource === 'worldbook-initvar')
|
|
468
|
+
|
|
469
|
+
// ★ `enabled: false` 的豁免是**有意**的,不是漏判:
|
|
470
|
+
// `异世界农场` 要读的那条本身就是 `enabled: false`(comment 还写着「勿开」)。
|
|
471
|
+
// 作者的约定是「把变量树放进一个禁用的条目,避免它被注入上下文,工具链仍可读取」。
|
|
472
|
+
// 若要求条目必须启用,这条兜底在唯一需要它的卡上就是空转(见 repro-wb-initvar.mjs 实测)。
|
|
473
|
+
// 挡住垃圾的是 `[initvar]` 标记 + 上面的形状判定,不是 enabled。
|
|
474
|
+
check('★ disabled 的 [initvar] 条目照样读(作者的约定就是把它关掉)',
|
|
475
|
+
parseMuvCard({
|
|
476
|
+
data: { name: 'dis', first_mes: '没变量', character_book: { entries: [{ comment: '[initvar]变量初始化勿开', content: '甲: 1', enabled: false }] } },
|
|
477
|
+
}).schemaSource === 'worldbook-initvar')
|
|
478
|
+
check('★ enabled 的 [initvar] 条目也读(两种都认)',
|
|
479
|
+
parseMuvCard({
|
|
480
|
+
data: { name: 'en', first_mes: '没变量', character_book: { entries: [{ comment: '[initvar]变量初始化', content: '甲: 1', enabled: true }] } },
|
|
481
|
+
}).schemaSource === 'worldbook-initvar')
|
|
482
|
+
check('★ disabled 且没有 [initvar] 标记的条目永远不读',
|
|
483
|
+
parseMuvCard({
|
|
484
|
+
data: { name: 'dis2', first_mes: '没变量', character_book: { entries: [{ comment: '变量初始化', content: '甲: 1', enabled: false }] } },
|
|
485
|
+
}).schemaSource === 'none')
|
|
486
|
+
|
|
371
487
|
console.log(`\n=== 结果: ${pass} 通过, ${fail} 失败 ===`)
|
|
372
488
|
process.exit(fail ? 1 : 0)
|