@linxin666/dsh-pet 0.3.4 → 0.3.6
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/README.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/contracts/voice-pack-v1.schema.json +53 -28
- package/cordis.patch.yml +1 -1
- package/lib/client.js +166 -30
- package/lib/client.js.map +1 -1
- package/lib/index.js +400 -478
- package/lib/invariant.js +1 -1
- package/lib/{state-DrMX22GL.js → state-NvDEoCln.js} +5 -2
- package/lib/types/access.d.ts +5 -4
- package/lib/types/access.d.ts.map +1 -1
- package/lib/types/access.js +2 -12
- package/lib/types/affinity.d.ts +2 -0
- package/lib/types/affinity.d.ts.map +1 -1
- package/lib/types/affinity.js +12 -0
- package/lib/types/chatter.d.ts +68 -46
- package/lib/types/chatter.d.ts.map +1 -1
- package/lib/types/chatter.js +243 -301
- package/lib/types/client/PetSprite.d.ts.map +1 -1
- package/lib/types/client/PetSprite.js +23 -18
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +26 -9
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +18 -0
- package/lib/types/client/renderers/frames2d.d.ts +12 -0
- package/lib/types/client/renderers/frames2d.d.ts.map +1 -1
- package/lib/types/client/renderers/frames2d.js +143 -18
- package/lib/types/event-projection.d.ts +9 -4
- package/lib/types/event-projection.d.ts.map +1 -1
- package/lib/types/event-projection.js +46 -14
- package/lib/types/ledger.d.ts +4 -2
- package/lib/types/ledger.d.ts.map +1 -1
- package/lib/types/ledger.js +4 -4
- package/lib/types/pair-access.d.ts +35 -0
- package/lib/types/pair-access.d.ts.map +1 -0
- package/lib/types/pair-access.js +19 -0
- package/lib/types/remarks.d.ts +1 -1
- package/lib/types/remarks.d.ts.map +1 -1
- package/lib/types/remarks.js +11 -2
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +8 -3
- package/lib/types/service.d.ts +7 -11
- package/lib/types/service.d.ts.map +1 -1
- package/lib/types/service.js +31 -20
- package/lib/types/voice-pack.d.ts +11 -7
- package/lib/types/voice-pack.d.ts.map +1 -1
- package/lib/types/voice-pack.js +80 -49
- package/package.json +1 -1
- package/src/access.ts +7 -27
- package/src/affinity.ts +13 -0
- package/src/chatter.test.ts +68 -38
- package/src/chatter.ts +269 -316
- package/src/client/PetSprite.test.tsx +50 -15
- package/src/client/PetSprite.tsx +30 -25
- package/src/client/index.test.tsx +10 -1
- package/src/client/index.ts +29 -10
- package/src/client/locales.ts +18 -0
- package/src/client/renderers/frames2d.test.ts +101 -0
- package/src/client/renderers/frames2d.ts +138 -18
- package/src/event-projection.ts +57 -16
- package/src/ledger.ts +6 -4
- package/src/pair-access.ts +49 -0
- package/src/remarks.test.ts +10 -0
- package/src/remarks.ts +9 -2
- package/src/routes.ts +9 -4
- package/src/service.ts +36 -30
- package/src/voice-pack.test.ts +87 -25
- package/src/voice-pack.ts +87 -49
package/lib/types/voice-pack.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* TypeScript mode: keep it erasable-syntax-only.
|
|
28
28
|
* @module @linxin666/dsh-pet/voice-pack
|
|
29
29
|
*/
|
|
30
|
-
import { STATUS_SCENES, TOOL_CATEGORIES, } from "./chatter.js";
|
|
30
|
+
import { STATUS_SCENES, TOOL_CATEGORIES, WHISPER_CATEGORIES, WHISPER_RESULTS, } from "./chatter.js";
|
|
31
|
+
import { normalizePetRemarks } from "./remarks.js";
|
|
31
32
|
/** Schema version this module normalizes (optional field; missing = 1). */
|
|
32
33
|
export const VOICE_PACK_V1 = 1;
|
|
33
34
|
/** Hover-panel action buttons a pack can show or hide (canonical order). */
|
|
@@ -39,9 +40,6 @@ export const PANEL_STAT_KEYS = ['rank', 'treats', 'points'];
|
|
|
39
40
|
/** Hard caps shared by every pool slot (mirrors the remarks discipline). */
|
|
40
41
|
export const VOICE_POOL_LINES_MAX = 64;
|
|
41
42
|
export const VOICE_LINE_MAX = 160;
|
|
42
|
-
export const VOICE_KEYWORDS_PER_RULE_MAX = 16;
|
|
43
|
-
export const VOICE_KEYWORD_MAX = 40;
|
|
44
|
-
export const VOICE_RULES_MAX = 32;
|
|
45
43
|
export const VOICE_LABEL_MAX = 40;
|
|
46
44
|
export const VOICE_STAT_MAX = 80;
|
|
47
45
|
/** Any '{token}' placeholder (no nesting, no newlines). */
|
|
@@ -112,51 +110,45 @@ export function normalizePool(raw, kind, onWarning = () => { }) {
|
|
|
112
110
|
}
|
|
113
111
|
return pool;
|
|
114
112
|
}
|
|
115
|
-
/** Normalize
|
|
116
|
-
export function
|
|
113
|
+
/** Normalize whisper category pools; a key replaces that category's built-in pool (an explicit empty pool mutes it). */
|
|
114
|
+
export function normalizeWhisperCategories(raw, onWarning = () => { }) {
|
|
117
115
|
if (raw === undefined)
|
|
118
116
|
return undefined;
|
|
119
|
-
if (!
|
|
120
|
-
onWarning('whispers.
|
|
117
|
+
if (!isRecord(raw)) {
|
|
118
|
+
onWarning('whispers.categories must be an object');
|
|
121
119
|
return undefined;
|
|
122
120
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (const item of raw.slice(0, VOICE_RULES_MAX)) {
|
|
128
|
-
if (!isRecord(item)) {
|
|
129
|
-
onWarning('whisper rule must be an object');
|
|
121
|
+
const pools = {};
|
|
122
|
+
for (const key of Object.keys(raw)) {
|
|
123
|
+
if (!WHISPER_CATEGORIES.includes(key)) {
|
|
124
|
+
onWarning('unknown whisper category ' + key + ' ignored');
|
|
130
125
|
continue;
|
|
131
126
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const pool = normalizePool(item.pool, 'whisperRule', onWarning);
|
|
153
|
-
if (keywords.length === 0 || pool === undefined || pool.length === 0) {
|
|
154
|
-
onWarning('whisper rule dropped (needs keywords and a non-empty pool)');
|
|
127
|
+
const pool = normalizePool(raw[key], 'whisperCategory', onWarning);
|
|
128
|
+
if (pool !== undefined)
|
|
129
|
+
pools[key] = pool;
|
|
130
|
+
}
|
|
131
|
+
return Object.keys(pools).length > 0 ? pools : undefined;
|
|
132
|
+
}
|
|
133
|
+
/** Normalize whisper outcome pools; a key replaces that outcome's built-in pool (an explicit empty pool mutes it). */
|
|
134
|
+
export function normalizeWhisperResults(raw, onWarning = () => { }) {
|
|
135
|
+
if (raw === undefined)
|
|
136
|
+
return undefined;
|
|
137
|
+
if (!isRecord(raw)) {
|
|
138
|
+
onWarning('whispers.results must be an object');
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
const pools = {};
|
|
142
|
+
for (const key of Object.keys(raw)) {
|
|
143
|
+
if (!WHISPER_RESULTS.includes(key)) {
|
|
144
|
+
onWarning('unknown whisper result ' + key + ' ignored');
|
|
155
145
|
continue;
|
|
156
146
|
}
|
|
157
|
-
|
|
147
|
+
const pool = normalizePool(raw[key], 'whisperResult', onWarning);
|
|
148
|
+
if (pool !== undefined)
|
|
149
|
+
pools[key] = pool;
|
|
158
150
|
}
|
|
159
|
-
return
|
|
151
|
+
return Object.keys(pools).length > 0 ? pools : undefined;
|
|
160
152
|
}
|
|
161
153
|
/** Normalize the panel block (labels / stats / actions; warn-and-drop). */
|
|
162
154
|
export function normalizePanel(raw, onWarning = () => { }) {
|
|
@@ -234,9 +226,9 @@ export function normalizePanel(raw, onWarning = () => { }) {
|
|
|
234
226
|
return panel;
|
|
235
227
|
}
|
|
236
228
|
/** Voice-pack top-level fields ('$schema' mirrors the schema twin; drift-locked in tests). */
|
|
237
|
-
export const VOICE_PACK_KEYS = new Set(['$schema', 'voicePackVersion', 'status', 'tools', 'toolRemaining', 'whispers', 'panel']);
|
|
229
|
+
export const VOICE_PACK_KEYS = new Set(['$schema', 'voicePackVersion', 'status', 'tools', 'toolRemaining', 'whispers', 'panel', 'remarks', 'ranks']);
|
|
238
230
|
/** Allowed whisper-section fields (drift-locked in tests). */
|
|
239
|
-
export const WHISPER_KEYS = new Set(['
|
|
231
|
+
export const WHISPER_KEYS = new Set(['categories', 'results']);
|
|
240
232
|
/**
|
|
241
233
|
* Normalize one raw voice.json document into a VoicePack, or undefined when
|
|
242
234
|
* the file cannot serve as a pack at all (non-object root — structure is
|
|
@@ -307,28 +299,57 @@ export function normalizeVoicePack(raw, onWarning = () => { }) {
|
|
|
307
299
|
}
|
|
308
300
|
else {
|
|
309
301
|
for (const key of Object.keys(whispersRaw)) {
|
|
310
|
-
if (
|
|
302
|
+
if (key === 'generic' || key === 'rules') {
|
|
303
|
+
// pet M6: keyword and ambient whisper channels are gone; legacy
|
|
304
|
+
// fields are ignored with a warning, never mapped to the new shape.
|
|
305
|
+
onWarning('whispers.' + key + ' is no longer supported and was ignored');
|
|
306
|
+
}
|
|
307
|
+
else if (!WHISPER_KEYS.has(key)) {
|
|
311
308
|
onWarning('unknown whispers field ' + key + ' ignored');
|
|
309
|
+
}
|
|
312
310
|
}
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
if (
|
|
311
|
+
const categories = normalizeWhisperCategories(whispersRaw.categories, onWarning);
|
|
312
|
+
const results = normalizeWhisperResults(whispersRaw.results, onWarning);
|
|
313
|
+
if (categories !== undefined || results !== undefined) {
|
|
316
314
|
overrides.whispers = {
|
|
317
|
-
...(
|
|
318
|
-
...(
|
|
315
|
+
...(categories === undefined ? {} : { categories }),
|
|
316
|
+
...(results === undefined ? {} : { results }),
|
|
319
317
|
};
|
|
320
318
|
}
|
|
321
319
|
}
|
|
322
320
|
}
|
|
323
321
|
const panel = raw.panel === undefined ? undefined : normalizePanel(raw.panel, onWarning);
|
|
322
|
+
const remarks = raw.remarks === undefined ? undefined : normalizePetRemarks(raw.remarks, onWarning);
|
|
323
|
+
const ranksRaw = raw.ranks;
|
|
324
|
+
let ranks;
|
|
325
|
+
if (ranksRaw !== undefined) {
|
|
326
|
+
if (!isRecord(ranksRaw)) {
|
|
327
|
+
onWarning('ranks must be an object');
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
const out = {};
|
|
331
|
+
for (const [key, value] of Object.entries(ranksRaw)) {
|
|
332
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
333
|
+
out[key] = value.trim().slice(0, VOICE_STAT_MAX);
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
onWarning('invalid rank name for ' + key);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
if (Object.keys(out).length > 0)
|
|
340
|
+
ranks = out;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
324
343
|
if (overrides.status === undefined && overrides.tools === undefined
|
|
325
344
|
&& overrides.toolRemaining === undefined && overrides.whispers === undefined
|
|
326
|
-
&& panel === undefined) {
|
|
345
|
+
&& panel === undefined && remarks === undefined && ranks === undefined) {
|
|
327
346
|
return undefined;
|
|
328
347
|
}
|
|
329
348
|
return {
|
|
330
349
|
overrides,
|
|
331
350
|
...(panel === undefined ? {} : { panel }),
|
|
351
|
+
...(remarks === undefined ? {} : { remarks }),
|
|
352
|
+
...(ranks === undefined ? {} : { ranks }),
|
|
332
353
|
};
|
|
333
354
|
}
|
|
334
355
|
/**
|
|
@@ -341,6 +362,8 @@ export function mergeVoicePacks(...layers) {
|
|
|
341
362
|
const overrides = {};
|
|
342
363
|
const labels = {};
|
|
343
364
|
const stats = {};
|
|
365
|
+
let remarks;
|
|
366
|
+
let ranks;
|
|
344
367
|
let actions;
|
|
345
368
|
let panelSeen = false;
|
|
346
369
|
let any = false;
|
|
@@ -368,6 +391,12 @@ export function mergeVoicePacks(...layers) {
|
|
|
368
391
|
if (layer.panel.actions !== undefined)
|
|
369
392
|
actions = layer.panel.actions;
|
|
370
393
|
}
|
|
394
|
+
if (layer.remarks !== undefined) {
|
|
395
|
+
remarks = { ...(remarks ?? {}), ...layer.remarks };
|
|
396
|
+
}
|
|
397
|
+
if (layer.ranks !== undefined) {
|
|
398
|
+
ranks = { ...(ranks ?? {}), ...layer.ranks };
|
|
399
|
+
}
|
|
371
400
|
}
|
|
372
401
|
if (!any)
|
|
373
402
|
return undefined;
|
|
@@ -380,5 +409,7 @@ export function mergeVoicePacks(...layers) {
|
|
|
380
409
|
return {
|
|
381
410
|
overrides,
|
|
382
411
|
...(panelSeen && !panelEmpty ? { panel } : {}),
|
|
412
|
+
...(remarks !== undefined ? { remarks } : {}),
|
|
413
|
+
...(ranks !== undefined ? { ranks } : {}),
|
|
383
414
|
};
|
|
384
415
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@linxin666/dsh-pet",
|
|
3
3
|
"description": "Multi-pet companion plugin for the dsh web GUI: a registry-driven floating pet that reacts to model activity, with per-pet naming, petting/feeding interactions and an affinity score",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
7
7
|
"node": "^22.19.0 || >=24.0.0"
|
package/src/access.ts
CHANGED
|
@@ -1,23 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pet trust fence: loopback (the desktop) always passes; a live
|
|
3
|
-
* cookie is an additional allow path when remote-web-ui is
|
|
4
|
-
* never depends on that plugin — without the service the
|
|
5
|
-
* loopback-only
|
|
2
|
+
* Pet trust fence: loopback (the desktop) always passes; a live
|
|
3
|
+
* paired-device cookie is an additional allow path when remote-web-ui is
|
|
4
|
+
* loaded. The pet never depends on that plugin — without the service the
|
|
5
|
+
* fence stays loopback-only. The decision logic lives in the generated
|
|
6
|
+
* pair-access.ts copy (shared by git-graph / pet / skill-explorer).
|
|
6
7
|
*/
|
|
7
8
|
import type { IncomingMessage } from 'node:http'
|
|
8
9
|
import type { Context } from '@deepseek-ai/cordis'
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
/** Structural pairing lookup (no package dependency on remote-web-ui). */
|
|
12
|
-
interface PairingAccess {
|
|
13
|
-
isPairedDevice(request: IncomingMessage): boolean
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** ctx.get is optional on the test harness; production Context always has it. */
|
|
17
|
-
type LookupCtx = Context & {
|
|
18
|
-
get?(name: string, strict?: boolean): unknown
|
|
19
|
-
remoteWebUiPairing?: PairingAccess
|
|
20
|
-
}
|
|
10
|
+
import { isPairedOrLoopbackAllowed } from './pair-access.ts'
|
|
21
11
|
|
|
22
12
|
/**
|
|
23
13
|
* Whether this request may enter any /api/pet or /pet asset route.
|
|
@@ -26,15 +16,5 @@ type LookupCtx = Context & {
|
|
|
26
16
|
* @returns true for loopback, or a live paired-device cookie.
|
|
27
17
|
*/
|
|
28
18
|
export function isPetAllowed(ctx: Context, request: IncomingMessage): boolean {
|
|
29
|
-
|
|
30
|
-
const bag = ctx as LookupCtx
|
|
31
|
-
const fromGet = typeof bag.get === 'function' ? bag.get('remoteWebUiPairing', false) : undefined
|
|
32
|
-
const pairing = (isPairingAccess(fromGet) ? fromGet : bag.remoteWebUiPairing)
|
|
33
|
-
return pairing?.isPairedDevice(request) === true
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isPairingAccess(value: unknown): value is PairingAccess {
|
|
37
|
-
return value !== undefined
|
|
38
|
-
&& value !== null
|
|
39
|
-
&& typeof (value as PairingAccess).isPairedDevice === 'function'
|
|
19
|
+
return isPairedOrLoopbackAllowed(ctx, request)
|
|
40
20
|
}
|
package/src/affinity.ts
CHANGED
|
@@ -56,6 +56,19 @@ export const AFFINITY_RANKS = [
|
|
|
56
56
|
{ min: 100_000, name: '鲸生共渡', emoji: '*********' },
|
|
57
57
|
] as const
|
|
58
58
|
|
|
59
|
+
/** English translations for the 9 built-in affinity rank names (#1226). */
|
|
60
|
+
export const AFFINITY_RANKS_EN: Readonly<Record<string, string>> = {
|
|
61
|
+
'幼鲸': 'Baby Whale',
|
|
62
|
+
'伙伴': 'Companion',
|
|
63
|
+
'挚友': 'Close Friend',
|
|
64
|
+
'深海羁绊': 'Deep Sea Bond',
|
|
65
|
+
'心有灵犀': 'Kindred Spirit',
|
|
66
|
+
'传说羁绊': 'Legendary Bond',
|
|
67
|
+
'神话羁绊': 'Mythic Bond',
|
|
68
|
+
'永恒之契': 'Eternal Covenant',
|
|
69
|
+
'鲸生共渡': 'Lifelong Companion',
|
|
70
|
+
}
|
|
71
|
+
|
|
59
72
|
/** Interaction tuning (all in points / ms). */
|
|
60
73
|
export interface AffinityConfig {
|
|
61
74
|
/** Points per completed turn. */
|
package/src/chatter.test.ts
CHANGED
|
@@ -11,8 +11,9 @@ import {
|
|
|
11
11
|
TOOL_POOLS,
|
|
12
12
|
toolArgHint,
|
|
13
13
|
toolCategory,
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
WHISPER_CATEGORY_POOLS,
|
|
15
|
+
WHISPER_RESULT_POOLS,
|
|
16
|
+
looksLikeTestTool,
|
|
16
17
|
WhisperEngine,
|
|
17
18
|
type VoicePackOverrides,
|
|
18
19
|
} from './chatter.ts'
|
|
@@ -127,30 +128,65 @@ describe('toolArgHint', () => {
|
|
|
127
128
|
})
|
|
128
129
|
|
|
129
130
|
describe('WhisperEngine', () => {
|
|
130
|
-
it('
|
|
131
|
+
it('speaks a category line for the current situation, then respects the cooldown', () => {
|
|
131
132
|
const engine = new WhisperEngine()
|
|
132
|
-
expect(engine.feed('
|
|
133
|
+
expect(engine.feed('thinking', 0)).toBe(WHISPER_CATEGORY_POOLS.thinking[0])
|
|
134
|
+
expect(engine.feed('writing', 1)).toBeUndefined()
|
|
133
135
|
})
|
|
134
136
|
|
|
135
|
-
it('
|
|
137
|
+
it('rotates within each category and keeps the category cursors apart', () => {
|
|
136
138
|
const engine = new WhisperEngine()
|
|
137
|
-
expect(engine.feed('
|
|
138
|
-
expect(engine.feed('
|
|
139
|
-
expect(engine.feed('
|
|
139
|
+
expect(engine.feed('thinking', 0)).toBe(WHISPER_CATEGORY_POOLS.thinking[0])
|
|
140
|
+
expect(engine.feed('writing', 9001)).toBe(WHISPER_CATEGORY_POOLS.writing[0])
|
|
141
|
+
expect(engine.feed('thinking', 18002)).toBe(WHISPER_CATEGORY_POOLS.thinking[1])
|
|
140
142
|
})
|
|
141
143
|
|
|
142
|
-
it('
|
|
143
|
-
const engine = new WhisperEngine(
|
|
144
|
-
expect(engine.feed('
|
|
145
|
-
expect(engine.feed('bbbb', 1)).toBe(WHISPER_GENERIC_POOL[0])
|
|
146
|
-
// The counter resets after speaking: another budget must accumulate.
|
|
147
|
-
expect(engine.feed('cccc', 2)).toBeUndefined()
|
|
148
|
-
expect(engine.feed('dddddddddd', 3)).toBe(WHISPER_GENERIC_POOL[1])
|
|
144
|
+
it('stays quiet on an explicitly muted category', () => {
|
|
145
|
+
const engine = new WhisperEngine(() => ({ whispers: { categories: { thinking: [] } } }))
|
|
146
|
+
expect(engine.feed('thinking', 0)).toBeUndefined()
|
|
149
147
|
})
|
|
150
148
|
|
|
151
|
-
it('
|
|
152
|
-
const engine = new WhisperEngine(
|
|
153
|
-
expect(engine.
|
|
149
|
+
it('wakes outcome whispers on their own shorter cooldown and rotates per outcome', () => {
|
|
150
|
+
const engine = new WhisperEngine()
|
|
151
|
+
expect(engine.result('pass', 0)).toBe(WHISPER_RESULT_POOLS.pass[0])
|
|
152
|
+
expect(engine.result('fail', 1000)).toBeUndefined()
|
|
153
|
+
expect(engine.result('fail', 5001)).toBe(WHISPER_RESULT_POOLS.fail[0])
|
|
154
|
+
expect(engine.result('pass', 10002)).toBe(WHISPER_RESULT_POOLS.pass[1])
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('paces category and outcome whispers against one shared clock', () => {
|
|
158
|
+
const engine = new WhisperEngine()
|
|
159
|
+
expect(engine.feed('writing', 0)).toBe(WHISPER_CATEGORY_POOLS.writing[0])
|
|
160
|
+
// An outcome right after a category reply must wait the outcome cooldown.
|
|
161
|
+
expect(engine.result('done', 4999)).toBeUndefined()
|
|
162
|
+
expect(engine.result('done', 5000)).toBe(WHISPER_RESULT_POOLS.done[0])
|
|
163
|
+
// After an outcome, the category must wait the full category cooldown
|
|
164
|
+
// measured from the outcome whisper (5000), so 14000 is the next slot.
|
|
165
|
+
expect(engine.feed('thinking', 5001)).toBeUndefined()
|
|
166
|
+
expect(engine.feed('thinking', 14000)).toBe(WHISPER_CATEGORY_POOLS.thinking[0])
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it('mutes an outcome with an explicit empty pool', () => {
|
|
170
|
+
const engine = new WhisperEngine(() => ({ whispers: { results: { fail: [] } } }))
|
|
171
|
+
expect(engine.result('fail', 0)).toBeUndefined()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('looksLikeTestTool', () => {
|
|
176
|
+
it('recognizes test tools by name and by shell command', () => {
|
|
177
|
+
expect(looksLikeTestTool('run_tests', undefined)).toBe(true)
|
|
178
|
+
expect(looksLikeTestTool('run_code', '{"command":"npm test"}')).toBe(true)
|
|
179
|
+
expect(looksLikeTestTool('run_code', '{"command":"pnpm run test -- --run"}')).toBe(true)
|
|
180
|
+
expect(looksLikeTestTool('bash', '{"command":"pytest -q"}')).toBe(true)
|
|
181
|
+
expect(looksLikeTestTool('bash', '{"command":"go test ./..."}')).toBe(true)
|
|
182
|
+
expect(looksLikeTestTool('run_code', '{"code":"import pytest"}')).toBe(true)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('does not misfire on ordinary tools or mentions in generic code', () => {
|
|
186
|
+
expect(looksLikeTestTool('bash', '{"command":"node server.js"}')).toBe(false)
|
|
187
|
+
expect(looksLikeTestTool('grep', '{"pattern":"test"}')).toBe(false)
|
|
188
|
+
expect(looksLikeTestTool('read', '{"file_path":"/repo/test/fixture.txt"}')).toBe(false)
|
|
189
|
+
expect(looksLikeTestTool('run_code', '{"code":"server.listen(8080)"}')).toBe(false)
|
|
154
190
|
})
|
|
155
191
|
})
|
|
156
192
|
|
|
@@ -165,8 +201,8 @@ describe('voice-pack overrides (pet-center M4)', () => {
|
|
|
165
201
|
},
|
|
166
202
|
toolRemaining: ['后台还有 {n} 个'],
|
|
167
203
|
whispers: {
|
|
168
|
-
|
|
169
|
-
|
|
204
|
+
categories: { thinking: ['自定义思考', '自定义思考二'] },
|
|
205
|
+
results: { pass: ['自定义全绿'] },
|
|
170
206
|
},
|
|
171
207
|
})
|
|
172
208
|
|
|
@@ -214,28 +250,22 @@ describe('voice-pack overrides (pet-center M4)', () => {
|
|
|
214
250
|
expect(voice.scene('done', 5000)).toBe('换声了')
|
|
215
251
|
})
|
|
216
252
|
|
|
217
|
-
it('replaces
|
|
253
|
+
it('replaces whisper category pools per key', () => {
|
|
218
254
|
const engine = new WhisperEngine(PACK)
|
|
219
|
-
expect(engine.feed('
|
|
220
|
-
|
|
221
|
-
expect(engine.feed('
|
|
255
|
+
expect(engine.feed('thinking', 0)).toBe('自定义思考')
|
|
256
|
+
expect(engine.feed('writing', 9000)).toBe(WHISPER_CATEGORY_POOLS.writing[0])
|
|
257
|
+
expect(engine.feed('thinking', 18000)).toBe('自定义思考二')
|
|
222
258
|
})
|
|
223
259
|
|
|
224
|
-
it('replaces
|
|
225
|
-
const engine = new WhisperEngine(PACK
|
|
226
|
-
expect(engine.
|
|
227
|
-
expect(engine.
|
|
228
|
-
})
|
|
229
|
-
|
|
230
|
-
it('mutes ambient whispers when the generic pool is explicitly empty', () => {
|
|
231
|
-
const engine = new WhisperEngine(() => ({ whispers: { generic: [] } }), 0, 3)
|
|
232
|
-
expect(engine.feed('aaaa', 0)).toBeUndefined()
|
|
233
|
-
expect(engine.feed('bbbb', 1)).toBeUndefined()
|
|
260
|
+
it('replaces whisper outcome pools per key', () => {
|
|
261
|
+
const engine = new WhisperEngine(PACK)
|
|
262
|
+
expect(engine.result('pass', 0)).toBe('自定义全绿')
|
|
263
|
+
expect(engine.result('fail', 5001)).toBe(WHISPER_RESULT_POOLS.fail[0])
|
|
234
264
|
})
|
|
235
265
|
|
|
236
|
-
it('
|
|
237
|
-
const engine = new WhisperEngine(() => ({ whispers: {
|
|
238
|
-
|
|
239
|
-
expect(engine.
|
|
266
|
+
it('mutes a whisper channel with an explicit empty override', () => {
|
|
267
|
+
const engine = new WhisperEngine(() => ({ whispers: { categories: { thinking: [] }, results: { done: [] } } }))
|
|
268
|
+
expect(engine.feed('thinking', 0)).toBeUndefined()
|
|
269
|
+
expect(engine.result('done', 0)).toBeUndefined()
|
|
240
270
|
})
|
|
241
271
|
})
|