dsh-muv-table 0.2.9 → 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 +268 -34
- package/lib/block-generator.js +8 -0
- package/lib/client.js +4 -1
- package/lib/index.js +141 -23
- package/lib/initvar-parser.js +68 -10
- package/lib/muv-parser.js +219 -30
- package/lib/panel.html +10 -1
- package/package.json +2 -2
- package/test-muv-parser.mjs +488 -0
- package/test-preset-resolve.mjs +210 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
// Regression: preset resolution in `/api/muv-table/tavern-card`.
|
|
2
|
+
//
|
|
3
|
+
// Run: node test-preset-resolve.mjs
|
|
4
|
+
//
|
|
5
|
+
// ② 的实测症状:`?presetId=does-not-exist` 会静默返回 tavern-lite 的「川上富江」
|
|
6
|
+
// (HTTP 200 / found:true / cardName=川上富江),用户以为在看自己的卡。
|
|
7
|
+
// 预设改名或重名(历史上「深渊区」→「精简酒馆」)同样中招。
|
|
8
|
+
//
|
|
9
|
+
// 这个用例**不碰用户真实环境**:先把 DSH_HOME 指到一个临时目录,造出假预设,
|
|
10
|
+
// 再动态 import 插件(模块级 PRESETS_ROOT 是 import 时确定的),然后走真实路由。
|
|
11
|
+
// 因此它的结果在任何机器上一致,而不是「本机恰好有哪张卡」。
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
import os from 'node:os'
|
|
16
|
+
|
|
17
|
+
let pass = 0, fail = 0
|
|
18
|
+
function check(name, cond, detail) {
|
|
19
|
+
if (cond) { pass++; console.log(' OK ' + name) }
|
|
20
|
+
else { fail++; console.log(' FAIL ' + name + (detail ? ' -> ' + detail : '')) }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
console.log('=== 预设定位回归(临时 DSH_HOME,不碰真实环境)===\n')
|
|
24
|
+
|
|
25
|
+
// ── 临时环境 ──────────────────────────────────────────────
|
|
26
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'dsh-muv-preset-'))
|
|
27
|
+
const presets = path.join(tmp, '.agent-presets')
|
|
28
|
+
const cardsDir = path.join(tmp, 'cards')
|
|
29
|
+
fs.mkdirSync(presets, { recursive: true })
|
|
30
|
+
fs.mkdirSync(cardsDir, { recursive: true })
|
|
31
|
+
|
|
32
|
+
const PERSONA_YML = [
|
|
33
|
+
'name: "@deepseek-ai/dsh-persona"',
|
|
34
|
+
'text: |-',
|
|
35
|
+
' 角色名:测试预设',
|
|
36
|
+
' # 没有 <initvar>,变量树从 characters.json 取',
|
|
37
|
+
'',
|
|
38
|
+
].join('\n')
|
|
39
|
+
|
|
40
|
+
/** A preset directory with one card in tavern's own store. */
|
|
41
|
+
function writePreset(id, cardName, desc) {
|
|
42
|
+
const dir = path.join(presets, id)
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
44
|
+
fs.writeFileSync(path.join(dir, 'agent.cordis.yml'), PERSONA_YML, 'utf8')
|
|
45
|
+
fs.writeFileSync(path.join(dir, 'characters.json'), JSON.stringify([
|
|
46
|
+
{ name: cardName, desc, first: '', enabled: true },
|
|
47
|
+
]), 'utf8')
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const LIST_DESC = [
|
|
51
|
+
'开场。',
|
|
52
|
+
'<initvar>',
|
|
53
|
+
'主角状态:',
|
|
54
|
+
' 名字: 测试',
|
|
55
|
+
'人际交往:',
|
|
56
|
+
' 最近互动记录: ',
|
|
57
|
+
' - 第一条互动',
|
|
58
|
+
' - 第二条互动',
|
|
59
|
+
'</initvar>',
|
|
60
|
+
].join('\n')
|
|
61
|
+
|
|
62
|
+
writePreset('tavern-lite', 'ZZ-MUV-DEFAULT-CARD', '<initvar>\n默认卡: 1\n</initvar>')
|
|
63
|
+
writePreset('real-preset', 'ZZ-MUV-REAL-CARD', LIST_DESC)
|
|
64
|
+
writePreset('malformed-preset', 'ZZ-MUV-MALFORMED-CARD', '没有变量')
|
|
65
|
+
|
|
66
|
+
// A library card whose `alternate_greetings` is an object instead of a list — the
|
|
67
|
+
// shape that used to make parseMuvCard throw and the endpoint answer 500.
|
|
68
|
+
fs.writeFileSync(path.join(cardsDir, 'ZZ-MUV-MALFORMED-CARD.json'), JSON.stringify({
|
|
69
|
+
data: { name: 'ZZ-MUV-MALFORMED-CARD', first_mes: 'hi', alternate_greetings: { a: 'x' } },
|
|
70
|
+
}), 'utf8')
|
|
71
|
+
|
|
72
|
+
// The same trick with a card that is fine, to prove the library lookup still works.
|
|
73
|
+
fs.writeFileSync(path.join(cardsDir, 'ZZ-MUV-LIB-CARD.json'), JSON.stringify({
|
|
74
|
+
data: { name: 'ZZ-MUV-LIB-CARD', first_mes: '<initvar>\n库卡: 1\n</initvar>' },
|
|
75
|
+
}), 'utf8')
|
|
76
|
+
writePreset('lib-preset', 'ZZ-MUV-LIB-CARD', '没有变量')
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(path.join(presets, 'session-bindings.json'), JSON.stringify({
|
|
79
|
+
'session-bound-real': 'real-preset',
|
|
80
|
+
'session-bound-gone': 'renamed-away', // 预设改过名 → 绑定悬空
|
|
81
|
+
'session-bound-default': 'default', // 酒馆语义:default ⇒ tavern-lite
|
|
82
|
+
}), 'utf8')
|
|
83
|
+
|
|
84
|
+
process.env.DSH_HOME = tmp
|
|
85
|
+
process.env.DSH_MUV_CARD_DIRS = cardsDir
|
|
86
|
+
|
|
87
|
+
const { apply } = await import('./lib/index.js')
|
|
88
|
+
|
|
89
|
+
const routes = []
|
|
90
|
+
apply({ get: () => undefined, webServer: { register: (r) => routes.push(r) } })
|
|
91
|
+
const route = routes.find(r => r.path === '/api/muv-table/tavern-card')
|
|
92
|
+
check('路由已注册', !!route, routes.map(r => r.path).join(' '))
|
|
93
|
+
|
|
94
|
+
async function call(qs) {
|
|
95
|
+
let status = 0, raw = ''
|
|
96
|
+
await route.handler(
|
|
97
|
+
{ method: 'GET', url: '/api/muv-table/tavern-card' + qs },
|
|
98
|
+
{ writeHead: (s) => { status = s }, end: (b) => { raw = String(b) } },
|
|
99
|
+
)
|
|
100
|
+
let body = null
|
|
101
|
+
try { body = JSON.parse(raw) } catch (_) {}
|
|
102
|
+
return { status, body }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function show(label, r) {
|
|
106
|
+
console.log(' ' + label + ' -> HTTP ' + r.status + ' ' + JSON.stringify({
|
|
107
|
+
ok: r.body?.ok, found: r.body?.found, cardName: r.body?.cardName,
|
|
108
|
+
presetDir: r.body?.presetDir, presetSource: r.body?.presetSource, error: r.body?.error,
|
|
109
|
+
}))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── 1) 显式 presetId 不存在:必须明说,不许换卡 ─────────────
|
|
113
|
+
console.log('\n[1] ?presetId=does-not-exist')
|
|
114
|
+
const r1 = await call('?presetId=does-not-exist')
|
|
115
|
+
show('返回', r1)
|
|
116
|
+
check('HTTP 200(不是 500)', r1.status === 200, 'status=' + r1.status)
|
|
117
|
+
check('found:false', r1.body?.found === false, 'found=' + r1.body?.found)
|
|
118
|
+
check('错误里点名了那个 id',
|
|
119
|
+
typeof r1.body?.error === 'string' && r1.body.error.includes('does-not-exist'),
|
|
120
|
+
JSON.stringify(r1.body?.error))
|
|
121
|
+
check('reason=preset-not-found', r1.body?.reason === 'preset-not-found', r1.body?.reason)
|
|
122
|
+
check('★没有偷偷换成默认预设的卡',
|
|
123
|
+
r1.body?.cardName !== 'ZZ-MUV-DEFAULT-CARD' && r1.body?.presetDir === undefined,
|
|
124
|
+
'cardName=' + r1.body?.cardName + ' presetDir=' + r1.body?.presetDir)
|
|
125
|
+
check('顺便给出真实存在的预设清单',
|
|
126
|
+
Array.isArray(r1.body?.availablePresets) && r1.body.availablePresets.includes('real-preset'),
|
|
127
|
+
JSON.stringify(r1.body?.availablePresets))
|
|
128
|
+
|
|
129
|
+
// ── 2) 合法 preset:数据必须完整到手(① 也一起走通) ────────
|
|
130
|
+
console.log('\n[2] ?presetId=real-preset(合法)')
|
|
131
|
+
const r2 = await call('?presetId=real-preset')
|
|
132
|
+
show('返回', r2)
|
|
133
|
+
check('found:true', r2.body?.found === true, 'found=' + r2.body?.found)
|
|
134
|
+
check('cardName 正确', r2.body?.cardName === 'ZZ-MUV-REAL-CARD', r2.body?.cardName)
|
|
135
|
+
check('presetDir 正确', r2.body?.presetDir === 'real-preset', r2.body?.presetDir)
|
|
136
|
+
check('presetSource=explicit', r2.body?.presetSource === 'explicit', r2.body?.presetSource)
|
|
137
|
+
const listVal = r2.body?.initvarData?.人际交往?.最近互动记录
|
|
138
|
+
check('★列表内容端到端到手(①)', Array.isArray(listVal) && listVal.length === 2,
|
|
139
|
+
JSON.stringify(listVal))
|
|
140
|
+
const listField = (r2.body?.schemas || []).find(s => s.name === '人际交往')
|
|
141
|
+
?.children?.find(c => c.name === '最近互动记录')
|
|
142
|
+
check('列表进了 schema', !!listField && listField.isList === true,
|
|
143
|
+
JSON.stringify(listField))
|
|
144
|
+
|
|
145
|
+
// ── 3) 路径穿越 ───────────────────────────────────────────
|
|
146
|
+
console.log('\n[3] ?presetId=../tavern-lite(越界)')
|
|
147
|
+
const r3 = await call('?presetId=' + encodeURIComponent('../tavern-lite'))
|
|
148
|
+
show('返回', r3)
|
|
149
|
+
check('found:false(不解析带分隔符的 id)', r3.body?.found === false, 'found=' + r3.body?.found)
|
|
150
|
+
|
|
151
|
+
// ── 4) 会话绑定 ──────────────────────────────────────────
|
|
152
|
+
console.log('\n[4] ?sessionId=session-bound-real')
|
|
153
|
+
const r4 = await call('?sessionId=session-bound-real')
|
|
154
|
+
show('返回', r4)
|
|
155
|
+
check('found:true', r4.body?.found === true, 'found=' + r4.body?.found)
|
|
156
|
+
check('按绑定取到 real-preset', r4.body?.presetDir === 'real-preset', r4.body?.presetDir)
|
|
157
|
+
check('presetSource=session', r4.body?.presetSource === 'session', r4.body?.presetSource)
|
|
158
|
+
|
|
159
|
+
console.log('\n[5] ?sessionId=session-bound-gone(绑定指向一个已改名的预设)')
|
|
160
|
+
const r5 = await call('?sessionId=session-bound-gone')
|
|
161
|
+
show('返回', r5)
|
|
162
|
+
check('found:false(悬空绑定不再静默换卡)', r5.body?.found === false, 'found=' + r5.body?.found)
|
|
163
|
+
check('错误里点名了那个已消失的预设',
|
|
164
|
+
typeof r5.body?.error === 'string' && r5.body.error.includes('renamed-away'),
|
|
165
|
+
JSON.stringify(r5.body?.error))
|
|
166
|
+
check('★没有偷偷换成默认预设的卡', r5.body?.cardName !== 'ZZ-MUV-DEFAULT-CARD', 'cardName=' + r5.body?.cardName)
|
|
167
|
+
|
|
168
|
+
console.log('\n[6] ?sessionId=session-bound-default(绑定是 default)')
|
|
169
|
+
const r6 = await call('?sessionId=session-bound-default')
|
|
170
|
+
show('返回', r6)
|
|
171
|
+
check('found:true(酒馆自己把 default 当 tavern-lite)', r6.body?.found === true, 'found=' + r6.body?.found)
|
|
172
|
+
check('落在 tavern-lite', r6.body?.presetDir === 'tavern-lite', r6.body?.presetDir)
|
|
173
|
+
check('★来源标明 session-default(不是谎称显式指定)',
|
|
174
|
+
r6.body?.presetSource === 'session-default', r6.body?.presetSource)
|
|
175
|
+
|
|
176
|
+
console.log('\n[7] ?sessionId=session-unbound(完全没有绑定记录)')
|
|
177
|
+
const r7 = await call('?sessionId=session-unbound')
|
|
178
|
+
show('返回', r7)
|
|
179
|
+
check('结果可解释:要么 found:false,要么标成 session-default',
|
|
180
|
+
r7.body?.found === false || r7.body?.presetSource === 'session-default',
|
|
181
|
+
'found=' + r7.body?.found + ' source=' + r7.body?.presetSource)
|
|
182
|
+
check('绝不谎称 explicit', r7.body?.presetSource !== 'explicit', r7.body?.presetSource)
|
|
183
|
+
|
|
184
|
+
console.log('\n[8] 两个定位参数同时给:presetId 优先(客户端只该给一个)')
|
|
185
|
+
const r8 = await call('?presetId=real-preset&sessionId=session-bound-default')
|
|
186
|
+
show('返回', r8)
|
|
187
|
+
check('用 presetId 那个', r8.body?.presetDir === 'real-preset' && r8.body?.presetSource === 'explicit',
|
|
188
|
+
r8.body?.presetDir + '/' + r8.body?.presetSource)
|
|
189
|
+
|
|
190
|
+
// ── 9) 畸形卡不再 500(④ 的端点面) ────────────────────────
|
|
191
|
+
console.log('\n[9] 畸形卡(alternate_greetings 是对象)走端点')
|
|
192
|
+
const r9 = await call('?presetId=malformed-preset')
|
|
193
|
+
show('返回', r9)
|
|
194
|
+
check('HTTP 200(不是 500)', r9.status === 200, 'status=' + r9.status + ' err=' + r9.body?.error)
|
|
195
|
+
check('仍返回同一张卡、只是变量表为空',
|
|
196
|
+
r9.body?.found === true && r9.body?.cardName === 'ZZ-MUV-MALFORMED-CARD' && (r9.body?.schemas || []).length === 0,
|
|
197
|
+
JSON.stringify({ found: r9.body?.found, name: r9.body?.cardName, schemas: r9.body?.schemas?.length }))
|
|
198
|
+
|
|
199
|
+
console.log('\n[10] 同一张卡库查找仍然正常(别把库路径改坏)')
|
|
200
|
+
const r10 = await call('?presetId=lib-preset')
|
|
201
|
+
show('返回', r10)
|
|
202
|
+
check('从卡库按名找到', r10.body?.cardSource === 'library', r10.body?.cardSource)
|
|
203
|
+
check('变量读得到', Object.keys(r10.body?.initvarData || {}).join(',') === '库卡',
|
|
204
|
+
Object.keys(r10.body?.initvarData || {}).join(','))
|
|
205
|
+
|
|
206
|
+
// 清理
|
|
207
|
+
try { fs.rmSync(tmp, { recursive: true, force: true }) } catch (_) {}
|
|
208
|
+
|
|
209
|
+
console.log(`\n=== 结果: ${pass} 通过, ${fail} 失败 ===`)
|
|
210
|
+
process.exit(fail ? 1 : 0)
|