dsh-plugin-tlrp 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.md +218 -2
- package/client.js +2079 -0
- package/cordis.patch.yml +18 -0
- package/icon.svg +18 -0
- package/index.cjs +37 -0
- package/index.js +34 -0
- package/lib/core.cjs +2636 -0
- package/lib/memory-bridge.cjs +265 -0
- package/lib/resolve.cjs +74 -0
- package/lib/schema.cjs +192 -0
- package/locale/en.json +20 -0
- package/locale/zh.json +20 -0
- package/package.json +73 -3
- package/scripts/apply-check.mjs +400 -0
- package/scripts/client-check.mjs +502 -0
- package/scripts/install-into-profile.mjs +167 -0
- package/scripts/mutate-check.mjs +284 -0
- package/scripts/rewrite-persona.mjs +106 -0
- package/test/core.test.mjs +270 -0
- package/test/memory-bridge.test.mjs +77 -0
- package/test/style.test.mjs +336 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// packages/tlrp/test/memory-bridge.test.mjs
|
|
2
|
+
//
|
|
3
|
+
// 记忆桥自测:不依赖宿主、不依赖 tlmemory 的代码,只用 node:test + 真实的
|
|
4
|
+
// ~/.dsh/tlmemory.db(可用 TLMEMORY_DB 覆盖)。库不存在时相关用例自动 skip,
|
|
5
|
+
// 这样 CI/新机上依然是绿的。
|
|
6
|
+
//
|
|
7
|
+
// 运行:node --test packages/tlrp/test/memory-bridge.test.mjs
|
|
8
|
+
|
|
9
|
+
import test from 'node:test'
|
|
10
|
+
import assert from 'node:assert/strict'
|
|
11
|
+
import fs from 'node:fs'
|
|
12
|
+
import os from 'node:os'
|
|
13
|
+
import path from 'node:path'
|
|
14
|
+
import { createRequire } from 'node:module'
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url)
|
|
17
|
+
const bridge = require('../lib/memory-bridge.cjs')
|
|
18
|
+
|
|
19
|
+
const dbPath = bridge.resolveDbPath()
|
|
20
|
+
const dbReady = fs.existsSync(dbPath)
|
|
21
|
+
|
|
22
|
+
test('extractTerms 只产出 ≥3 字的检索锚点', () => {
|
|
23
|
+
const terms = bridge.extractTerms('你还记得橡木蛋糕卷吗?上次在筑梦边境的天台')
|
|
24
|
+
assert.ok(terms.length > 0, '应当抽出至少一个词')
|
|
25
|
+
for (const term of terms) assert.ok([...term].length >= 3, `词 "${term}" 少于 3 字,trigram 索引不到`)
|
|
26
|
+
assert.ok(terms.some((term) => term.includes('蛋糕')), `期望命中「蛋糕」,实际:${terms.join(',')}`)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('extractTerms 对短句与空输入不抛错', () => {
|
|
30
|
+
assert.deepEqual(bridge.extractTerms(''), [])
|
|
31
|
+
assert.deepEqual(bridge.extractTerms('嗯'), [])
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
test('库文件不存在时降级为 no-db,且不抛错', () => {
|
|
35
|
+
const result = bridge.readMemoryContext({
|
|
36
|
+
characterId: 'firefly',
|
|
37
|
+
mode: 'rp',
|
|
38
|
+
text: '橡木蛋糕卷',
|
|
39
|
+
dbPath: path.join(os.tmpdir(), 'definitely-not-a-tlmemory-db.sqlite'),
|
|
40
|
+
})
|
|
41
|
+
assert.equal(result.text, '')
|
|
42
|
+
assert.equal(result.degraded, 'no-db')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('RP 态读到 pinned 常驻羁绊(真实库)', { skip: dbReady ? false : `无 ${dbPath}` }, () => {
|
|
46
|
+
const result = bridge.readMemoryContext({ characterId: 'firefly', mode: 'rp', text: '' })
|
|
47
|
+
assert.equal(result.degraded, '', `降级码:${result.degraded}`)
|
|
48
|
+
assert.ok(result.pinned.length >= 1, '至少应读到 1 条 pinned 记忆')
|
|
49
|
+
assert.ok(
|
|
50
|
+
result.pinned.some((item) => item.path.startsWith('/rp/firefly/')),
|
|
51
|
+
`pinned 里应含 /rp/firefly/ 前缀:${result.pinned.map((i) => i.path).join(' | ')}`,
|
|
52
|
+
)
|
|
53
|
+
assert.ok(result.text.includes('tlmemory 记忆桥'))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
test('RP 态关键词静默检索命中 lore 切片(真实库)', { skip: dbReady ? false : `无 ${dbPath}` }, () => {
|
|
57
|
+
const result = bridge.readMemoryContext({ characterId: 'firefly', mode: 'rp', text: '橡木蛋糕卷' })
|
|
58
|
+
const all = [...result.pinned, ...result.hits]
|
|
59
|
+
assert.ok(
|
|
60
|
+
all.some((item) => item.path.startsWith('/lore/firefly/')),
|
|
61
|
+
`应命中 lore 切片:${all.map((i) => i.path).join(' | ')}`,
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('WORK 态不会捞到 rp / lore 分区(真实库)', { skip: dbReady ? false : `无 ${dbPath}` }, () => {
|
|
66
|
+
const result = bridge.readMemoryContext({ characterId: 'firefly', mode: 'work', text: '橡木蛋糕卷 端口' })
|
|
67
|
+
for (const item of [...result.pinned, ...result.hits]) {
|
|
68
|
+
assert.ok(item.scope === 'work' || item.scope === 'core', `WORK 态出现了 ${item.scope} 分区:${item.path}`)
|
|
69
|
+
}
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
test('角色隔离:别的角色读不到 firefly 的私有 rp 记忆(真实库)', { skip: dbReady ? false : `无 ${dbPath}` }, () => {
|
|
73
|
+
const result = bridge.readMemoryContext({ characterId: 'sam_tactical', mode: 'rp', text: '橡木蛋糕卷' })
|
|
74
|
+
for (const item of [...result.pinned, ...result.hits]) {
|
|
75
|
+
assert.ok(item.charId === '' || item.charId === 'sam_tactical', `串戏了:${item.path} (char_id=${item.charId})`)
|
|
76
|
+
}
|
|
77
|
+
})
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import { test } from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { mkdtempSync, writeFileSync, rmSync, mkdirSync } from 'node:fs'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
|
|
8
|
+
const require = createRequire(import.meta.url)
|
|
9
|
+
const core = require('../lib/core.cjs')
|
|
10
|
+
|
|
11
|
+
const readFrom = (map) => (field) => (field in map ? map[field] : undefined)
|
|
12
|
+
const firefly = core.findCharacter(core.DEFAULT_CHARACTERS, 'firefly')
|
|
13
|
+
|
|
14
|
+
const baseConfig = (extra = {}) => ({
|
|
15
|
+
enabled: true,
|
|
16
|
+
mode: 'work',
|
|
17
|
+
modes: [
|
|
18
|
+
{ id: 'work', name: '战术工作态', commandAliases: ['/work'], injectCharacterPersona: false, enableStatusFooter: false },
|
|
19
|
+
{ id: 'rp', name: '陪伴态', commandAliases: ['/rp'], injectCharacterPersona: true },
|
|
20
|
+
],
|
|
21
|
+
characters: core.DEFAULT_CHARACTERS,
|
|
22
|
+
activeCharacterId: 'firefly',
|
|
23
|
+
workModeKeywords: core.DEFAULT_WORK_KEYWORDS,
|
|
24
|
+
triggers: [],
|
|
25
|
+
timeZone: 'Asia/Shanghai',
|
|
26
|
+
statusFooterLabel: '流萤状态同步',
|
|
27
|
+
maxTriggerInjections: 3,
|
|
28
|
+
enableMemoryBridge: false,
|
|
29
|
+
enableStatusFooter: true,
|
|
30
|
+
customSceneOverride: '',
|
|
31
|
+
styleLayerDefault: 'off',
|
|
32
|
+
styleEmojiHint: '一屏最多 2 个 emoji。',
|
|
33
|
+
stickerDir: '',
|
|
34
|
+
galleryDir: '',
|
|
35
|
+
maxStickersPerTurn: 2,
|
|
36
|
+
...extra,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
/* ── 开关解析 ── */
|
|
40
|
+
|
|
41
|
+
test('resolveStyleLayer:会话覆盖 > 形态自身 > 全局默认', () => {
|
|
42
|
+
assert.deepEqual(core.resolveStyleLayer({ modeLayer: 'off', globalDefault: 'on', sessionOverride: 'on' }), {
|
|
43
|
+
on: true,
|
|
44
|
+
dialog: false,
|
|
45
|
+
source: 'session',
|
|
46
|
+
})
|
|
47
|
+
assert.deepEqual(core.resolveStyleLayer({ modeLayer: 'on', globalDefault: 'off' }), {
|
|
48
|
+
on: true,
|
|
49
|
+
dialog: false,
|
|
50
|
+
source: 'mode',
|
|
51
|
+
})
|
|
52
|
+
assert.deepEqual(core.resolveStyleLayer({ modeLayer: 'inherit', globalDefault: 'on' }), {
|
|
53
|
+
on: true,
|
|
54
|
+
dialog: false,
|
|
55
|
+
source: 'global',
|
|
56
|
+
})
|
|
57
|
+
assert.deepEqual(core.resolveStyleLayer({}), { on: false, dialog: false, source: 'global' })
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('normalizeStyleLayerFlag 兼容布尔值与大小写,未知值回落 inherit', () => {
|
|
61
|
+
assert.equal(core.normalizeStyleLayerFlag(true), 'on')
|
|
62
|
+
assert.equal(core.normalizeStyleLayerFlag(false), 'off')
|
|
63
|
+
assert.equal(core.normalizeStyleLayerFlag('OFF'), 'off')
|
|
64
|
+
assert.equal(core.normalizeStyleLayerFlag(' On '), 'on')
|
|
65
|
+
assert.equal(core.normalizeStyleLayerFlag('whatever'), 'inherit')
|
|
66
|
+
assert.equal(core.normalizeStyleLayerFlag(undefined), 'inherit')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
test('normalizeMode 把 styleLayer 带进形态定义', () => {
|
|
70
|
+
assert.equal(core.normalizeMode({ id: 'x', styleLayer: 'off' }).styleLayer, 'off')
|
|
71
|
+
assert.equal(core.normalizeMode({ id: 'x', styleLayer: 'on' }).styleLayer, 'on')
|
|
72
|
+
assert.equal(core.normalizeMode({ id: 'x' }).styleLayer, 'inherit')
|
|
73
|
+
// 内置形态默认跟随全局
|
|
74
|
+
for (const mode of core.normalizeModes(core.DEFAULT_MODES)) assert.equal(mode.styleLayer, 'inherit')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
/* ── 本地图库扫描 ── */
|
|
78
|
+
|
|
79
|
+
test('scanStickerLibrary 只认图片扩展名、跳过子目录、目录缺失降级为空', () => {
|
|
80
|
+
const dir = mkdtempSync(join(tmpdir(), 'tlrp-stickers-'))
|
|
81
|
+
try {
|
|
82
|
+
writeFileSync(join(dir, '开心.png'), 'x')
|
|
83
|
+
writeFileSync(join(dir, '无语.GIF'), 'x')
|
|
84
|
+
writeFileSync(join(dir, '说明.txt'), 'x')
|
|
85
|
+
mkdirSync(join(dir, 'nested'))
|
|
86
|
+
const items = core.scanStickerLibrary(dir)
|
|
87
|
+
assert.deepEqual(items.map((item) => item.tag).sort(), ['无语', '开心'].sort())
|
|
88
|
+
for (const item of items) {
|
|
89
|
+
assert.ok(item.path.includes('/'), `路径应统一成正斜杠:${item.path}`)
|
|
90
|
+
assert.ok(!item.path.includes('\\'))
|
|
91
|
+
}
|
|
92
|
+
assert.deepEqual(core.scanStickerLibrary(join(dir, 'missing')), [])
|
|
93
|
+
assert.deepEqual(core.scanStickerLibrary(''), [])
|
|
94
|
+
assert.equal(core.scanStickerLibrary(dir, { limit: 1 }).length, 1)
|
|
95
|
+
} finally {
|
|
96
|
+
rmSync(dir, { recursive: true, force: true })
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
test('stickerDirOf / galleryDirOf 缺省落到 ~/.dsh 下', () => {
|
|
101
|
+
const sticker = core.stickerDirOf('', 'firefly')
|
|
102
|
+
const gallery = core.galleryDirOf('')
|
|
103
|
+
assert.match(sticker.replace(/\\/g, '/'), /\/\.dsh\/stickers\/firefly$/)
|
|
104
|
+
assert.match(gallery.replace(/\\/g, '/'), /\/\.dsh\/gallery$/)
|
|
105
|
+
assert.equal(core.stickerDirOf('D:/mine', 'firefly'), 'D:/mine')
|
|
106
|
+
assert.equal(core.galleryDirOf('D:/pics'), 'D:/pics')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
/* ── 风格块渲染 ── */
|
|
110
|
+
|
|
111
|
+
test('renderStyleBlock:完整人设时只补表情与贴纸,精简时带上语言风格', () => {
|
|
112
|
+
const stickers = [{ tag: '开心', path: 'C:/x/stickers/开心.png' }]
|
|
113
|
+
const lean = core.renderStyleBlock(firefly, {
|
|
114
|
+
fullPersona: false,
|
|
115
|
+
emojiHint: '一屏最多 2 个',
|
|
116
|
+
stickers,
|
|
117
|
+
maxStickersPerTurn: 2,
|
|
118
|
+
})
|
|
119
|
+
assert.match(lean, /风格层/)
|
|
120
|
+
assert.match(lean, /语言风格与禁用项/)
|
|
121
|
+
assert.match(lean, /台词语气锚点/)
|
|
122
|
+
assert.match(lean, /一屏最多 2 个/)
|
|
123
|
+
assert.match(lean, /!\[开心\]\(C:\/x\/stickers\/开心\.png\)/)
|
|
124
|
+
assert.match(lean, /不写 \*斜体动作\*/)
|
|
125
|
+
|
|
126
|
+
const full = core.renderStyleBlock(firefly, {
|
|
127
|
+
fullPersona: true,
|
|
128
|
+
emojiHint: '一屏最多 2 个',
|
|
129
|
+
stickers,
|
|
130
|
+
maxStickersPerTurn: 2,
|
|
131
|
+
})
|
|
132
|
+
assert.doesNotMatch(full, /语言风格与禁用项/)
|
|
133
|
+
assert.doesNotMatch(full, /台词语气锚点/)
|
|
134
|
+
assert.match(full, /表情包(标签 → 可直接粘贴的行)/)
|
|
135
|
+
|
|
136
|
+
const silent = core.renderStyleBlock(firefly, { fullPersona: false, emojiHint: '', maxStickersPerTurn: 0 })
|
|
137
|
+
assert.match(silent, /本轮不使用 emoji/)
|
|
138
|
+
assert.match(silent, /本轮禁止贴任何表情包或图片/)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('renderStyleBlock 无图库时不编造路径', () => {
|
|
142
|
+
const text = core.renderStyleBlock(firefly, { fullPersona: true, emojiHint: 'ok', stickers: [], gallery: [], maxStickersPerTurn: 2 })
|
|
143
|
+
assert.doesNotMatch(text, /!\[/)
|
|
144
|
+
assert.doesNotMatch(text, /本地图库/)
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
/* ── 指令接线 ── */
|
|
148
|
+
|
|
149
|
+
test('parseCommand 识别 /style 子命令', () => {
|
|
150
|
+
const on = core.parseCommand('/style on')
|
|
151
|
+
assert.equal(on.kind, 'command')
|
|
152
|
+
assert.equal(on.command, '/style')
|
|
153
|
+
assert.equal(on.arg, 'on')
|
|
154
|
+
assert.equal(on.mode, '')
|
|
155
|
+
|
|
156
|
+
const status = core.parseCommand('/style')
|
|
157
|
+
assert.equal(status.command, '/style')
|
|
158
|
+
assert.equal(status.arg, '')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
/* ── 快照与注入 ── */
|
|
162
|
+
|
|
163
|
+
test('snapshot:风格层按会话 > 形态 > 全局解析,关掉时不产出 block', () => {
|
|
164
|
+
const state = core.createState({ read: readFrom(baseConfig({ styleLayerDefault: 'on' })) })
|
|
165
|
+
const agent = { session: { id: 's-style-global' } }
|
|
166
|
+
const on = core.snapshot(state, agent)
|
|
167
|
+
assert.equal(on.style.on, true)
|
|
168
|
+
assert.equal(on.style.source, 'global')
|
|
169
|
+
assert.match(on.style.block, /风格层/)
|
|
170
|
+
|
|
171
|
+
state.sessionState(agent).styleLayer = 'off'
|
|
172
|
+
const off = core.snapshot(state, agent)
|
|
173
|
+
assert.equal(off.style.on, false)
|
|
174
|
+
assert.equal(off.style.source, 'session')
|
|
175
|
+
assert.equal(off.style.block, '')
|
|
176
|
+
|
|
177
|
+
const modeState = core.createState({
|
|
178
|
+
read: readFrom(
|
|
179
|
+
baseConfig({
|
|
180
|
+
styleLayerDefault: 'on',
|
|
181
|
+
modes: [{ id: 'work', name: '战术工作态', injectCharacterPersona: false, styleLayer: 'off' }],
|
|
182
|
+
}),
|
|
183
|
+
),
|
|
184
|
+
})
|
|
185
|
+
const modeSnap = core.snapshot(modeState, { session: { id: 's-style-mode' } })
|
|
186
|
+
assert.equal(modeSnap.style.on, false)
|
|
187
|
+
assert.equal(modeSnap.style.source, 'mode')
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
test('snapshot:图库目录里的图片进入风格块并计数', () => {
|
|
191
|
+
const dir = mkdtempSync(join(tmpdir(), 'tlrp-gallery-'))
|
|
192
|
+
try {
|
|
193
|
+
writeFileSync(join(dir, '天台日落.png'), 'x')
|
|
194
|
+
const state = core.createState({
|
|
195
|
+
read: readFrom(baseConfig({ styleLayerDefault: 'on', galleryDir: dir, maxStickersPerTurn: 1 })),
|
|
196
|
+
})
|
|
197
|
+
const snap = core.snapshot(state, { session: { id: 's-gallery' } })
|
|
198
|
+
assert.equal(snap.style.galleryCount, 1)
|
|
199
|
+
assert.match(snap.style.block, /天台日落\.png/)
|
|
200
|
+
assert.match(snap.style.block, /每轮最多贴 1 张/)
|
|
201
|
+
} finally {
|
|
202
|
+
rmSync(dir, { recursive: true, force: true })
|
|
203
|
+
}
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
test('renderModeDirective 把风格层追加在形态准则末尾,不改形态红线', () => {
|
|
207
|
+
const styleBlock = core.renderStyleBlock(firefly, { fullPersona: false, emojiHint: 'ok', maxStickersPerTurn: 0 })
|
|
208
|
+
const text = core.renderModeDirective({
|
|
209
|
+
character: firefly,
|
|
210
|
+
mode: 'work',
|
|
211
|
+
modeDef: core.findMode(core.DEFAULT_MODES, 'work'),
|
|
212
|
+
scene: '测试场景',
|
|
213
|
+
triggers: [],
|
|
214
|
+
footerEnabled: false,
|
|
215
|
+
footerLabel: '测试',
|
|
216
|
+
style: { on: true, source: 'global', block: styleBlock },
|
|
217
|
+
})
|
|
218
|
+
assert.match(text, /运维纪律在本轮优先级高于人设/)
|
|
219
|
+
assert.ok(text.trimEnd().endsWith('本轮禁止贴任何表情包或图片。'))
|
|
220
|
+
|
|
221
|
+
const withoutStyle = core.renderModeDirective({
|
|
222
|
+
character: firefly,
|
|
223
|
+
mode: 'work',
|
|
224
|
+
modeDef: core.findMode(core.DEFAULT_MODES, 'work'),
|
|
225
|
+
scene: '测试场景',
|
|
226
|
+
triggers: [],
|
|
227
|
+
footerEnabled: false,
|
|
228
|
+
footerLabel: '测试',
|
|
229
|
+
})
|
|
230
|
+
assert.doesNotMatch(withoutStyle, /风格层/)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('renderTurnContext 报告风格层来源与图库数量', () => {
|
|
234
|
+
const text = core.renderTurnContext({
|
|
235
|
+
mode: 'work',
|
|
236
|
+
modeDef: core.findMode(core.DEFAULT_MODES, 'work'),
|
|
237
|
+
character: firefly,
|
|
238
|
+
scene: '测试场景',
|
|
239
|
+
triggers: [],
|
|
240
|
+
footerEnabled: false,
|
|
241
|
+
footerLabel: '测试',
|
|
242
|
+
reason: '测试',
|
|
243
|
+
now: '2026-11-02 星期一 18:00',
|
|
244
|
+
cwd: 'C:/tmp',
|
|
245
|
+
memory: { text: '' },
|
|
246
|
+
style: { on: true, source: 'session', block: '', stickerCount: 3, galleryCount: 1 },
|
|
247
|
+
})
|
|
248
|
+
assert.match(text, /风格层:开(来源:会话 \/style 覆盖)/)
|
|
249
|
+
assert.match(text, /表情包 3 张、图库 1 张/)
|
|
250
|
+
|
|
251
|
+
const offText = core.renderTurnContext({
|
|
252
|
+
mode: 'work',
|
|
253
|
+
modeDef: core.findMode(core.DEFAULT_MODES, 'work'),
|
|
254
|
+
character: firefly,
|
|
255
|
+
scene: '测试场景',
|
|
256
|
+
triggers: [],
|
|
257
|
+
footerEnabled: false,
|
|
258
|
+
footerLabel: '测试',
|
|
259
|
+
reason: '测试',
|
|
260
|
+
now: '2026-11-02 星期一 18:00',
|
|
261
|
+
cwd: 'C:/tmp',
|
|
262
|
+
memory: { text: '' },
|
|
263
|
+
style: { on: false, source: 'global', block: '', stickerCount: 0, galleryCount: 0 },
|
|
264
|
+
})
|
|
265
|
+
assert.match(offText, /风格层:关(来源:全局 styleLayerDefault)/)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
/* ── 对话体档(dialog)── */
|
|
269
|
+
|
|
270
|
+
test('dialog 档:别名归一、优先级与三分量解析', () => {
|
|
271
|
+
assert.equal(core.normalizeStyleLayerFlag('dialog'), 'dialog')
|
|
272
|
+
assert.equal(core.normalizeStyleLayerFlag('对话体'), 'dialog')
|
|
273
|
+
assert.equal(core.normalizeStyleLayerFlag('CHAT'), 'dialog')
|
|
274
|
+
|
|
275
|
+
assert.deepEqual(core.resolveStyleLayer({ modeLayer: 'dialog', globalDefault: 'off' }), {
|
|
276
|
+
on: true,
|
|
277
|
+
dialog: true,
|
|
278
|
+
source: 'mode',
|
|
279
|
+
})
|
|
280
|
+
assert.deepEqual(core.resolveStyleLayer({ globalDefault: 'dialog' }), { on: true, dialog: true, source: 'global' })
|
|
281
|
+
assert.deepEqual(core.resolveStyleLayer({ modeLayer: 'dialog', sessionOverride: 'on' }), {
|
|
282
|
+
on: true,
|
|
283
|
+
dialog: false,
|
|
284
|
+
source: 'session',
|
|
285
|
+
})
|
|
286
|
+
assert.equal(core.styleLayerFlagText('dialog'), '对话体(只说话,不演动作与旁白)')
|
|
287
|
+
})
|
|
288
|
+
|
|
289
|
+
test('renderStyleBlock 对话体:换标题、给表演强度红线、禁旁白', () => {
|
|
290
|
+
const text = core.renderStyleBlock(firefly, {
|
|
291
|
+
fullPersona: true,
|
|
292
|
+
emojiHint: 'ok',
|
|
293
|
+
dialogOnly: true,
|
|
294
|
+
maxStickersPerTurn: 0,
|
|
295
|
+
})
|
|
296
|
+
assert.match(text, /### 风格层(本轮生效:对话体,只说话不演)/)
|
|
297
|
+
assert.doesNotMatch(text, /只带语言风格,不演动作/)
|
|
298
|
+
assert.match(text, /本轮的表演强度/)
|
|
299
|
+
assert.match(text, /只输出「流萤[^」]*」说出口的话,不写旁白/)
|
|
300
|
+
assert.match(text, /禁止环境与天气描写/)
|
|
301
|
+
assert.match(text, /禁止外貌与服饰特写/)
|
|
302
|
+
assert.match(text, /状态面板照旧输出/)
|
|
303
|
+
|
|
304
|
+
// 非对话体不出现这段
|
|
305
|
+
const plain = core.renderStyleBlock(firefly, { fullPersona: true, emojiHint: 'ok', maxStickersPerTurn: 0 })
|
|
306
|
+
assert.doesNotMatch(plain, /本轮的表演强度/)
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
test('snapshot 对话体:全局档位透传到 style 与状态块', () => {
|
|
310
|
+
const state = core.createState({ read: readFrom(baseConfig({ styleLayerDefault: 'dialog' })) })
|
|
311
|
+
const snap = core.snapshot(state, { session: { id: 's-dialog' } })
|
|
312
|
+
assert.equal(snap.style.on, true)
|
|
313
|
+
assert.equal(snap.style.dialog, true)
|
|
314
|
+
assert.equal(snap.style.source, 'global')
|
|
315
|
+
assert.match(snap.style.block, /对话体/)
|
|
316
|
+
|
|
317
|
+
// 形态写 off 时对话体仍然被压掉
|
|
318
|
+
const modeState = core.createState({
|
|
319
|
+
read: readFrom(
|
|
320
|
+
baseConfig({
|
|
321
|
+
styleLayerDefault: 'dialog',
|
|
322
|
+
modes: [{ id: 'work', name: '战术工作态', injectCharacterPersona: false, styleLayer: 'off' }],
|
|
323
|
+
}),
|
|
324
|
+
),
|
|
325
|
+
})
|
|
326
|
+
const modeSnap = core.snapshot(modeState, { session: { id: 's-dialog-off' } })
|
|
327
|
+
assert.equal(modeSnap.style.on, false)
|
|
328
|
+
assert.equal(modeSnap.style.dialog, false)
|
|
329
|
+
assert.equal(modeSnap.style.block, '')
|
|
330
|
+
})
|
|
331
|
+
|
|
332
|
+
test('/style dialog 被解析成 dialog 档', () => {
|
|
333
|
+
const parsed = core.parseCommand('/style dialog')
|
|
334
|
+
assert.equal(parsed.command, '/style')
|
|
335
|
+
assert.equal(core.normalizeStyleLayerFlag(parsed.arg), 'dialog')
|
|
336
|
+
})
|