dsh-villager-hmm 1.0.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 +34 -0
- package/README.md +248 -0
- package/client/client.js +540 -0
- package/cordis.patch.yml +15 -0
- package/lib/index.js +391 -0
- package/package.json +76 -0
- package/scripts/fetch-assets.mjs +159 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 dsh-villager-hmm contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
NOTICE ON THIRD-PARTY ASSETS
|
|
26
|
+
|
|
27
|
+
This package bundles and redistributes no third-party assets.
|
|
28
|
+
|
|
29
|
+
scripts/fetch-assets.mjs downloads the Minecraft villager sounds and the
|
|
30
|
+
villager skin onto the operator's own machine, at the operator's direction,
|
|
31
|
+
into a local cache outside this package. Those files are the property of
|
|
32
|
+
Mojang Studios / Microsoft and are not covered by the MIT grant above; they
|
|
33
|
+
are never part of this package's source tree, tarball, or repository.
|
|
34
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# dsh-villager-hmm
|
|
2
|
+
|
|
3
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin.
|
|
4
|
+
Whenever the model goes *"hmm"* — inside its chain of thought, or in the reply
|
|
5
|
+
itself — a Minecraft villager hums back at you.
|
|
6
|
+
|
|
7
|
+
A small villager sits in the top-right corner, bobs on every hit, and carries
|
|
8
|
+
the controls: pause, volume, which stream to watch, and the matcher itself.
|
|
9
|
+
|
|
10
|
+
**Drag the title bar to move it; double-click the bar to send it back to the
|
|
11
|
+
corner.** The position is remembered in `localStorage`. It defaults to the
|
|
12
|
+
top-right rather than the bottom-right on purpose — the composer is full-width,
|
|
13
|
+
so a bottom-anchored overlay sits on top of the send button.
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
🧑🌾 村民 hmm 音效 触发 12 · 播放 12 -
|
|
17
|
+
思维链 1284 字 正文 356 字 音效 2 段
|
|
18
|
+
[ 监听中 · 点击暂停 ] [ 试听一次 ] 音量 ────●────
|
|
19
|
+
检测源 [ 仅思维链 ▾ ]
|
|
20
|
+
匹配式 [(?<![0-9A-Za-z])(?:h+m+|mhm+|m{2,})|[嗯唔哼呃] ] [应用] [默认]
|
|
21
|
+
最近命中 hmm 嗯 唔
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
# 1. install the package into your profile
|
|
28
|
+
dsh plugin --profile web add dsh-villager-hmm
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```jsonc
|
|
32
|
+
// 2. register it as a profile bundle — add the name to the
|
|
33
|
+
// `dsh.profile.bundles` array in $DSH_HOME/profiles/web/package.json
|
|
34
|
+
"dsh": { "profile": { "bundles": ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app", "dsh-villager-hmm"] } }
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
# 3. fetch the sounds and the villager texture (see Assets below)
|
|
39
|
+
npx dsh-villager-hmm-assets
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The package ships its own `cordis.patch.yml`, so step 2 is what actually
|
|
43
|
+
inserts the plugin row. Restart the profile after editing.
|
|
44
|
+
|
|
45
|
+
Skip step 2 and nothing happens; skip step 3 and the plugin loads, shows its
|
|
46
|
+
panel, and tells you which command to run — it never fails silently.
|
|
47
|
+
|
|
48
|
+
<details>
|
|
49
|
+
<summary>Installing from a local checkout instead of npm</summary>
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
git clone https://github.com/nagatoquin33/dsh-villager-hmm && cd dsh-villager-hmm
|
|
53
|
+
dsh plugin --profile web add "$PWD"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Note that a path containing spaces breaks pnpm's argument handling — install
|
|
57
|
+
from a space-free directory.
|
|
58
|
+
</details>
|
|
59
|
+
|
|
60
|
+
## Assets
|
|
61
|
+
|
|
62
|
+
**This package ships no Minecraft material.** The two villager sounds and the
|
|
63
|
+
villager skin are fetched on your machine into a per-user cache:
|
|
64
|
+
|
|
65
|
+
```
|
|
66
|
+
$DSH_HOME/.dsh-villager-hmm/assets/
|
|
67
|
+
idle1.ogg villager "hmm"
|
|
68
|
+
idle2.ogg villager "hmm", second take
|
|
69
|
+
villager.png the 64x64 villager skin; the panel crops the head front in CSS
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`npx dsh-villager-hmm-assets` (or `node scripts/fetch-assets.mjs`) populates it.
|
|
73
|
+
Two sources are supported:
|
|
74
|
+
|
|
75
|
+
| Flag | Meaning |
|
|
76
|
+
| --- | --- |
|
|
77
|
+
| *(none)* | Download from a public mirror of the vanilla 1.21.4 client assets |
|
|
78
|
+
| `--from <dir>` | Copy from an asset tree you extracted yourself — **the option that matches the game's terms** |
|
|
79
|
+
| `--base <url>` | Your own mirror with the same layout |
|
|
80
|
+
| `--dir <path>` | Use a different cache directory |
|
|
81
|
+
| `--force` | Re-fetch files that are already there |
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
# from your own extraction
|
|
85
|
+
npx dsh-villager-hmm-assets --from "C:/mc-extract/assets/minecraft"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The cache lives outside `node_modules`, so it survives plugin upgrades. The
|
|
89
|
+
host half re-reads it per request, which means running the fetch script takes
|
|
90
|
+
effect on a page reload — no harness restart.
|
|
91
|
+
|
|
92
|
+
## Configuration
|
|
93
|
+
|
|
94
|
+
Defaults live in the row. To override them, replace the insert in your own
|
|
95
|
+
`$DSH_HOME/profiles/web/cordis.patch.yml`:
|
|
96
|
+
|
|
97
|
+
```yaml
|
|
98
|
+
- insert:
|
|
99
|
+
- id: dsh-villager-hmm
|
|
100
|
+
name: 'dsh-villager-hmm'
|
|
101
|
+
config:
|
|
102
|
+
enabled: true
|
|
103
|
+
mode: both # 'reasoning' (chain of thought only) | 'both'
|
|
104
|
+
pattern: '(?<![0-9A-Za-z])(?:h+m+|mhm+|m{2,})|[嗯唔哼呃]'
|
|
105
|
+
assetDir: 'D:/my-assets'
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Everything except `assetDir` is also editable from the floating panel.
|
|
109
|
+
|
|
110
|
+
`mode` defaults to `both` on purpose: plenty of models expose no separate
|
|
111
|
+
reasoning channel, and a plugin that silently does nothing looks broken. Switch
|
|
112
|
+
to `reasoning` once you have confirmed your model streams `reasoning-delta` —
|
|
113
|
+
the panel's **思维链** counter tells you: if it climbs, you have a reasoning
|
|
114
|
+
channel and can narrow the scope.
|
|
115
|
+
|
|
116
|
+
## The matcher
|
|
117
|
+
|
|
118
|
+
The default pattern is
|
|
119
|
+
`(?<![0-9A-Za-z])(?:h+m+|mhm+|m{2,})|[嗯唔哼呃]`.
|
|
120
|
+
|
|
121
|
+
It uses a **lookbehind rather than `\b`**, for two reasons:
|
|
122
|
+
|
|
123
|
+
- `\b` is ASCII-only in JavaScript, so it behaves unpredictably next to CJK
|
|
124
|
+
characters — `嗯hmm` would not match.
|
|
125
|
+
- The CJK branch deliberately has no boundary at all, because those characters
|
|
126
|
+
are always standalone interjections.
|
|
127
|
+
|
|
128
|
+
`h+m+` covers `hm`, `hmm`, `hmmm…`; `mhm+` covers the closed-mouth variant;
|
|
129
|
+
`m{2,}` covers a bare `mm`. None of them fire inside ordinary words — `comma`,
|
|
130
|
+
`comment`, `command` and `ahmm` all stay silent.
|
|
131
|
+
|
|
132
|
+
Write your own in the panel's **匹配式** field and hit **应用**. A pattern that
|
|
133
|
+
does not compile is reported inline instead of being swallowed.
|
|
134
|
+
|
|
135
|
+
## How it works
|
|
136
|
+
|
|
137
|
+
The host half listens to the `agent/assistant-stream` event and scans
|
|
138
|
+
`reasoning-delta` (and optionally `text-delta`) chunks against the matcher. The
|
|
139
|
+
browser half polls that counter over the plugin's own HTTP prefix and plays a
|
|
140
|
+
sound per hit.
|
|
141
|
+
|
|
142
|
+
Two details are worth knowing if you touch the scanner:
|
|
143
|
+
|
|
144
|
+
**`frame.revision` is not a stream identity.** It increments on *every* frame
|
|
145
|
+
(`dsh-agent-loop`, `() => ++this.assistantStreamRevision`). Keying the scan
|
|
146
|
+
buffer by it resets the buffer on every delta, so the matcher only ever sees one
|
|
147
|
+
character and never matches. The buffer is keyed by `attemptId:turn:step`.
|
|
148
|
+
|
|
149
|
+
**The scan is inline, not debounced.** Deltas arrive far faster than any debounce
|
|
150
|
+
window, so every new delta cancels the pending timer before it can fire — on a
|
|
151
|
+
real stream, 1623 schedules produced 5 fires. Because the matcher keeps a
|
|
152
|
+
consumed-offset cursor, scanning on every delta is amortised O(n).
|
|
153
|
+
|
|
154
|
+
A match that ends exactly at the buffer end is deferred unless the attempt is
|
|
155
|
+
finishing, so `hm` is never counted before the next delta can turn it into
|
|
156
|
+
`hmm`; the block-end/end frames flush whatever is left.
|
|
157
|
+
|
|
158
|
+
## Layout
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
lib/index.js host half — event listener + HTTP routes
|
|
162
|
+
client/client.js browser half — prebuilt __ModuleLoader__ bundle
|
|
163
|
+
scripts/fetch-assets.mjs the asset fetcher (also the package's bin)
|
|
164
|
+
cordis.patch.yml the row this package inserts
|
|
165
|
+
test/self-test.mjs unit + integration tests
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Routes
|
|
169
|
+
|
|
170
|
+
The plugin owns one HTTP prefix, `/dsh-villager-hmm`:
|
|
171
|
+
|
|
172
|
+
| Route | Purpose |
|
|
173
|
+
| --- | --- |
|
|
174
|
+
| `GET /state?cursor=N` | counters, asset status, and triggers newer than `N` |
|
|
175
|
+
| `GET /config?enabled&mode&pattern` | change settings at runtime |
|
|
176
|
+
| `GET /sound/<n>.ogg` | the audio, read from the cache |
|
|
177
|
+
| `GET /texture.png` | the villager skin, read from the cache |
|
|
178
|
+
|
|
179
|
+
## Tests
|
|
180
|
+
|
|
181
|
+
```sh
|
|
182
|
+
npm test # 24 hermetic cases, no dependencies
|
|
183
|
+
npm run test:boot # mounts the host half in a real cordis Context
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`npm test` covers the scanner against character-by-character streams with
|
|
187
|
+
`revision` incrementing per frame, boundary and false-positive cases,
|
|
188
|
+
chunk-size invariance, config changes, the route surface, a populated and an
|
|
189
|
+
empty asset cache, that no assets are bundled, and the client bundle's
|
|
190
|
+
materialization.
|
|
191
|
+
|
|
192
|
+
`npm run test:boot` is the guard against a harness update breaking the row. It
|
|
193
|
+
imports and applies the real host half inside a live cordis `Context` and
|
|
194
|
+
checks that a missing `webServer`, a throwing `register()`, a malformed request
|
|
195
|
+
URL, and unknown chunk and frame kinds all degrade instead of failing the boot.
|
|
196
|
+
Cordis is an optional peer, so the suite skips cleanly when it is not
|
|
197
|
+
resolvable — point `DSH_CORDIS` at a deployment's copy to run it without
|
|
198
|
+
installing anything:
|
|
199
|
+
|
|
200
|
+
```sh
|
|
201
|
+
DSH_CORDIS=/path/to/node_modules/@deepseek-ai/cordis/lib/index.js npm run test:boot
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Releasing
|
|
205
|
+
|
|
206
|
+
CI (`.github/workflows/ci.yml`) runs the tests on every push and pull request
|
|
207
|
+
against Node 22 and 24. It also fails the build if a binary asset is ever
|
|
208
|
+
tracked, or if `dsh.bundle.patch` stops resolving — the two things this
|
|
209
|
+
repository must not get wrong.
|
|
210
|
+
|
|
211
|
+
Publishing is tag-driven:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
npm version patch # or minor / major — commits and creates the tag
|
|
215
|
+
git push --follow-tags
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
`.github/workflows/release.yml` then checks the tag against `package.json`,
|
|
219
|
+
runs the tests, asserts the tarball carries no Minecraft assets, publishes to
|
|
220
|
+
npm, and opens a GitHub release.
|
|
221
|
+
|
|
222
|
+
**One-time setup — trusted publishing.** No npm token is stored anywhere. The
|
|
223
|
+
workflow authenticates with a GitHub OIDC token that npm exchanges for a
|
|
224
|
+
short-lived registry token, and provenance is attached automatically because
|
|
225
|
+
both the repository and the package are public:
|
|
226
|
+
|
|
227
|
+
```sh
|
|
228
|
+
npm login
|
|
229
|
+
npm trust github dsh-villager-hmm --file release.yml --repo nagatoquin33/dsh-villager-hmm -y
|
|
230
|
+
npm trust list dsh-villager-hmm # verify
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
If `npm trust` refuses because the package does not exist yet, publish once by
|
|
234
|
+
hand (`npm publish`) and run the command above afterwards — every later release
|
|
235
|
+
goes through CI.
|
|
236
|
+
|
|
237
|
+
Prefer a long-lived token instead? Drop the `id-token: write` permission, add
|
|
238
|
+
an automation token as the `NPM_TOKEN` repository secret, and uncomment the
|
|
239
|
+
`env:` block on the Publish step.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
The code is MIT (see `LICENSE`).
|
|
244
|
+
|
|
245
|
+
No third-party assets are bundled or redistributed: `scripts/fetch-assets.mjs`
|
|
246
|
+
downloads them on the operator's machine, at the operator's direction, into a
|
|
247
|
+
local cache. Minecraft is a trademark of Mojang Studios; this project is not
|
|
248
|
+
affiliated with or endorsed by Mojang or Microsoft.
|