dsh-muv-table 0.2.6 → 0.2.7
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 +11 -0
- package/package.json +2 -2
- package/test-png-card.mjs +116 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v0.2.7 (2026-09-20)
|
|
4
|
+
|
|
5
|
+
### 📦 发布内容修正
|
|
6
|
+
|
|
7
|
+
- **把 `test-png-card.mjs` 纳入 npm 包**。0.2.6 的 README 让用户跑
|
|
8
|
+
`node test-png-card.mjs`,但 `files` 字段没列它,从 npm 安装的人拿不到。
|
|
9
|
+
现在一并发布,装完即可自测 PNG 卡解析是否正常(含真实卡库验证,卡库不存在时自动跳过)。
|
|
10
|
+
- `README.md` 显式列入 `files`。
|
|
11
|
+
|
|
12
|
+
> 0.2.6 的功能改动见下。
|
|
13
|
+
|
|
3
14
|
## v0.2.6 (2026-09-20)
|
|
4
15
|
|
|
5
16
|
### 🐛 修复:变量面板显示错误的卡(显示成苍玄界)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-muv-table",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"description": "MUV 变量表格编辑器 —— 树形变量面板、宏测试浮窗(random/pick/roll)、骰子快捷栏、pick 缓存管理;支持直接读取 SillyTavern PNG 角色卡(含正则脚本、世界书、变量表)。dsh-muv-engine 的伴生插件",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"./lib/*": "./lib/*",
|
|
11
11
|
"./package.json": "./package.json"
|
|
12
12
|
},
|
|
13
|
-
"files": ["lib", "cordis.patch.yml", "CHANGELOG.md"],
|
|
13
|
+
"files": ["lib", "cordis.patch.yml", "CHANGELOG.md", "README.md", "test-png-card.mjs"],
|
|
14
14
|
"keywords": ["dsh", "dsh-plugin", "deepseek-harness", "muv", "magvarupdate", "tavern", "variable-table"],
|
|
15
15
|
"license": "PolyForm-Noncommercial-Copyleft-1.0.0",
|
|
16
16
|
"dsh": {
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Regression for the PNG character-card reader.
|
|
2
|
+
//
|
|
3
|
+
// Run: node test-png-card.mjs
|
|
4
|
+
//
|
|
5
|
+
// The reader is what lets DSH see a SillyTavern card's *original* data —
|
|
6
|
+
// regex scripts, world book, helper scripts. DSH's own `characters.json`
|
|
7
|
+
// keeps only {name, desc, first}, so if this breaks, a card dragged into
|
|
8
|
+
// SillyTavern silently arrives with none of its rendering machinery.
|
|
9
|
+
import { readPngCard, readPngChunks, extractCharaPayload, isPngCard } from './lib/png-card.js'
|
|
10
|
+
import { readdirSync, existsSync } from 'node:fs'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import os from 'node:os'
|
|
13
|
+
|
|
14
|
+
let pass = 0, fail = 0
|
|
15
|
+
function check(name, cond, detail) {
|
|
16
|
+
if (cond) { pass++; console.log(' OK ' + name) }
|
|
17
|
+
else { fail++; console.log(' FAIL ' + name + (detail ? ' -> ' + detail : '')) }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
console.log('=== PNG 卡解析回归 ===\n')
|
|
21
|
+
|
|
22
|
+
// 1) Synthesise a PNG so the test does not depend on any particular machine's
|
|
23
|
+
// card library. Build the smallest valid chunk set with a `chara` tEXt.
|
|
24
|
+
function buildPng(cardObj) {
|
|
25
|
+
const chunks = []
|
|
26
|
+
const chunk = (type, data) => {
|
|
27
|
+
const len = Buffer.alloc(4); len.writeUInt32BE(data.length)
|
|
28
|
+
const t = Buffer.from(type, 'ascii')
|
|
29
|
+
const crc = Buffer.alloc(4) // CRC is not validated by the reader
|
|
30
|
+
return Buffer.concat([len, t, data, crc])
|
|
31
|
+
}
|
|
32
|
+
const ihdr = Buffer.alloc(13)
|
|
33
|
+
ihdr.writeUInt32BE(1, 0); ihdr.writeUInt32BE(1, 4)
|
|
34
|
+
chunks.push(chunk('IHDR', ihdr))
|
|
35
|
+
const b64 = Buffer.from(JSON.stringify(cardObj), 'utf8').toString('base64')
|
|
36
|
+
chunks.push(chunk('tEXt', Buffer.concat([Buffer.from('chara', 'latin1'), Buffer.from([0]), Buffer.from(b64, 'latin1')])))
|
|
37
|
+
chunks.push(chunk('IEND', Buffer.alloc(0)))
|
|
38
|
+
return Buffer.concat([Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]), ...chunks])
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const sampleCard = {
|
|
42
|
+
name: 'sample',
|
|
43
|
+
description: 'desc',
|
|
44
|
+
first_mes: 'hello',
|
|
45
|
+
data: {
|
|
46
|
+
name: 'sample',
|
|
47
|
+
description: 'desc text',
|
|
48
|
+
first_mes: 'hello there',
|
|
49
|
+
extensions: {
|
|
50
|
+
regex_scripts: [{ scriptName: 'a' }, { scriptName: 'b' }],
|
|
51
|
+
tavern_helper: { scripts: [{ name: 'x' }] },
|
|
52
|
+
},
|
|
53
|
+
character_book: { name: 'wb', entries: [{ comment: 'e1' }] },
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log('[1] 合成的 PNG 能被解析')
|
|
58
|
+
const buf = buildPng(sampleCard)
|
|
59
|
+
const chunks = readPngChunks(buf)
|
|
60
|
+
check('chunk 遍历出 IHDR/tEXt/IEND', chunks.map(c => c.type).join(',') === 'IHDR,tEXt,IEND', chunks.map(c => c.type).join(','))
|
|
61
|
+
const payload = extractCharaPayload(buf)
|
|
62
|
+
check('chara payload 提取成功', typeof payload === 'string' && payload.length > 10)
|
|
63
|
+
check('payload 是 base64', /^[A-Za-z0-9+/=]+$/.test(payload || ''))
|
|
64
|
+
const decoded = JSON.parse(Buffer.from(payload, 'base64').toString('utf8'))
|
|
65
|
+
check('解出正则数量=2', (decoded.data.extensions.regex_scripts || []).length === 2)
|
|
66
|
+
check('解出世界书', decoded.data.character_book?.entries?.length === 1)
|
|
67
|
+
check('解出 helper 脚本', (decoded.data.extensions.tavern_helper.scripts || []).length === 1)
|
|
68
|
+
|
|
69
|
+
console.log('\n[2] 不是卡的 PNG 返回 null(不能瞎认)')
|
|
70
|
+
const plainPng = Buffer.concat([
|
|
71
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
72
|
+
(() => { const l = Buffer.alloc(4); l.writeUInt32BE(13); const t = Buffer.from('IHDR'); const d = Buffer.alloc(13); return Buffer.concat([l, t, d, Buffer.alloc(4)]) })(),
|
|
73
|
+
(() => { const l = Buffer.alloc(4); l.writeUInt32BE(0); const t = Buffer.from('IEND'); return Buffer.concat([l, t, Buffer.alloc(4)]) })(),
|
|
74
|
+
])
|
|
75
|
+
check('无 chara 块 -> null', extractCharaPayload(plainPng) === null)
|
|
76
|
+
check('垃圾数据不抛异常', (() => { try { extractCharaPayload(Buffer.from('not a png')); return true } catch { return false } })())
|
|
77
|
+
|
|
78
|
+
console.log('\n[3] isPngCard')
|
|
79
|
+
check('.png 判定', isPngCard('a/b/c.png') === true)
|
|
80
|
+
check('.json 不判定为 png', isPngCard('a.json') === false)
|
|
81
|
+
|
|
82
|
+
console.log('\n[4] 真实卡库(存在才测,缺失则跳过)')
|
|
83
|
+
function sillyTavernDirs() {
|
|
84
|
+
const roots = [process.env.DSH_SILLYTAVERN_DIR, 'C:/MySpecialFolder/SillyTavern',
|
|
85
|
+
path.join(os.homedir(), 'SillyTavern'), path.join(os.homedir(), 'Documents', 'SillyTavern')].filter(Boolean)
|
|
86
|
+
const out = []
|
|
87
|
+
for (const root of roots) {
|
|
88
|
+
const dataDir = path.join(root, 'data')
|
|
89
|
+
if (!existsSync(dataDir)) continue
|
|
90
|
+
for (const user of readdirSync(dataDir)) {
|
|
91
|
+
const chars = path.join(dataDir, user, 'characters')
|
|
92
|
+
if (existsSync(chars)) out.push(chars)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
const dirs = sillyTavernDirs()
|
|
98
|
+
let realCards = 0, realWithRegex = 0
|
|
99
|
+
for (const d of dirs) {
|
|
100
|
+
for (const f of readdirSync(d)) {
|
|
101
|
+
if (!f.endsWith('.png')) continue
|
|
102
|
+
const card = readPngCard(path.join(d, f))
|
|
103
|
+
if (!card) continue
|
|
104
|
+
realCards++
|
|
105
|
+
if ((card.data.extensions?.regex_scripts || []).length > 0) realWithRegex++
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (realCards === 0) {
|
|
109
|
+
console.log(' SKIP 未找到 SillyTavern 卡库')
|
|
110
|
+
} else {
|
|
111
|
+
check('读到真实 PNG 卡 (' + realCards + ' 张)', realCards > 0)
|
|
112
|
+
check('其中带正则脚本的 (' + realWithRegex + ' 张)', realWithRegex > 0)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log(`\n=== 结果: ${pass} 通过, ${fail} 失败 ===`)
|
|
116
|
+
process.exit(fail ? 1 : 0)
|