@doitian/dsh-music 0.1.1 → 0.1.2
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.md +152 -34
- package/cordis.patch.yml +14 -0
- package/lib/client.js +48 -11
- package/lib/dj.js +234 -48
- package/lib/index.js +35 -2
- package/lib/netease.js +23 -7
- package/lib/panel.html +451 -212
- package/lib/router.js +7 -4
- package/lib/session.js +14 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
NetEase Cloud Music ([music.163.com](https://music.163.com)) playback and an AI DJ, inside DeepSeek Harness.
|
|
4
4
|
|
|
5
|
-
A **Music** entry appears in the DSH sidebar. It opens a
|
|
5
|
+
A **Music** entry appears in the DSH sidebar. It opens a player — your queue, synced lyrics, an `<audio>` element that streams through the host, QR sign-in, and a continuous AI DJ. The same player is also exposed to the agent through seven tools, so it can search, queue, and curate from a conversation.
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
|
|
9
|
-
│
|
|
10
|
-
│
|
|
11
|
-
│
|
|
12
|
-
│
|
|
13
|
-
│
|
|
14
|
-
│
|
|
15
|
-
|
|
8
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
9
|
+
│ 音乐 music.163.com [AI DJ] [mood] [account] │
|
|
10
|
+
│ │
|
|
11
|
+
│ ♪ 海屿你 — 马也_Crabbit │
|
|
12
|
+
│ 上一首 · 播放 · 下一首 · 列表 · 喜欢 · 静音 · 音量 │
|
|
13
|
+
│ │
|
|
14
|
+
│ 歌词 Lyrics │
|
|
15
|
+
│ [00:12.40] ... │
|
|
16
|
+
│ 播放队列 Queue (12) │
|
|
17
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
16
18
|
```
|
|
17
19
|
|
|
18
20
|
## Install
|
|
@@ -61,6 +63,15 @@ need no reinstall. Note that this is what makes a plugin *remount* re-read the
|
|
|
61
63
|
served page while the module stays cached — see
|
|
62
64
|
[Reloading a change](#reloading-a-change).
|
|
63
65
|
|
|
66
|
+
**Language.** The UI follows the Harness language (Settings → Language): the
|
|
67
|
+
sidebar label and error page register a zh/en dictionary with the host's
|
|
68
|
+
`locale` service, and the player page follows the shell's `<html lang>` live.
|
|
69
|
+
Switching language re-renders both in place — no restart.
|
|
70
|
+
|
|
71
|
+
While a profile is linked, the Desktop app's **Add plugin** flow is the wrong
|
|
72
|
+
tool for it: that flow installs the published tarball, which replaces the
|
|
73
|
+
`link:` dependency. Re-add the link if it happens.
|
|
74
|
+
|
|
64
75
|
### Either way
|
|
65
76
|
|
|
66
77
|
The bundle's own `cordis.patch.yml` inserts the plugin entry, so no
|
|
@@ -115,7 +126,7 @@ rich request could not be honoured. It never silently pretends, and it never
|
|
|
115
126
|
leaves you with silence because you asked for more than the track has.
|
|
116
127
|
|
|
117
128
|
The choice is saved in `session.json` and beats `config.audioLevel` in the
|
|
118
|
-
profile patch, the same precedence the
|
|
129
|
+
profile patch, the same precedence the DJ's model route uses. An unknown value in
|
|
119
130
|
either place falls back to `exhigh` rather than failing a stream.
|
|
120
131
|
|
|
121
132
|
Two implementation details worth knowing:
|
|
@@ -154,6 +165,28 @@ Then one of two tiers chooses:
|
|
|
154
165
|
affinity, novelty against play history, and explicit like/dislike feedback,
|
|
155
166
|
then interleaved so consecutive tracks do not share an artist.
|
|
156
167
|
|
|
168
|
+
**Every model call carries a session identity.** A leaf call cannot set headers
|
|
169
|
+
— `GenerateOptions` has no `headers` field — so `sessionId` is the only identity
|
|
170
|
+
a plugin can give a provider, and each adapter maps it onto whatever that
|
|
171
|
+
provider calls a per-conversation header: pi-ai emits `x-opencode-session` for
|
|
172
|
+
the `opencode-go` route. Without it, a provider that keys on the conversation
|
|
173
|
+
sees an anonymous client ignoring its conventions.
|
|
174
|
+
|
|
175
|
+
The identity is minted once per install and persisted in `session.json`, so
|
|
176
|
+
every plan and every restart stays inside one conversation; `dj.sessionId` pins
|
|
177
|
+
it explicitly instead. A request with no identity is refused rather than sent
|
|
178
|
+
bare, so the failure is a recorded `dj.modelError` rather than a silent one.
|
|
179
|
+
|
|
180
|
+
Two other properties are worth knowing:
|
|
181
|
+
|
|
182
|
+
- **The DJ is not an agent, and does not start one.** It is plugin code that
|
|
183
|
+
gathers candidates, makes one model call, and stocks the queue. Routing that
|
|
184
|
+
call through a DSH agent would buy conversation memory and an audit trail at
|
|
185
|
+
the cost of a session lifecycle (rotation, tool masking, disposal).
|
|
186
|
+
- **The model tier is stateless.** Each plan sends its own pool and gets its
|
|
187
|
+
picks back; there is no accumulated transcript, no context growth and no drift
|
|
188
|
+
from a previous mood — which is the property, not a limitation, for this job.
|
|
189
|
+
|
|
157
190
|
The model tier is optional and silent on failure; the DJ never blocks playback.
|
|
158
191
|
It only ever **stocks the queue** — it never starts or stops playback, so a
|
|
159
192
|
top-up while paused stays paused, and one that refills an empty queue after the
|
|
@@ -162,7 +195,7 @@ last track ended resumes on its own because the desired state was already
|
|
|
162
195
|
|
|
163
196
|
### Choosing the model
|
|
164
197
|
|
|
165
|
-
**In the player.** The right column carries a **AI DJ 模型 /
|
|
198
|
+
**In the player.** The right column carries a **AI DJ 模型 / DJ model**
|
|
166
199
|
panel: a provider picker, a model picker, and three buttons.
|
|
167
200
|
|
|
168
201
|
| Control | What it does |
|
|
@@ -201,21 +234,35 @@ control back to the patch.
|
|
|
201
234
|
Both names must be ones the host actually serves. `provider` is a registered
|
|
202
235
|
adapter route — read them from the `llm-pi-ai` entry's `config.providers` (or
|
|
203
236
|
ask the agent for `ctx.llm.listProviders()`); `model` is any id that route
|
|
204
|
-
accepts.
|
|
205
|
-
|
|
237
|
+
accepts.
|
|
238
|
+
|
|
239
|
+
**Pin it, or follow the session.** With both fields unset the DJ inherits
|
|
240
|
+
`agent-default-model` — the same selection the composer's model picker writes,
|
|
241
|
+
so it is the model the agent loop itself runs on — which is why "use my session
|
|
242
|
+
model" needs no configuration. Pinning `dj.provider`/`dj.model` curates on
|
|
243
|
+
something else instead, and pinning is what makes the two choices independent:
|
|
244
|
+
|
|
245
|
+
```yaml
|
|
246
|
+
- id: agent-default-model # the agent loop / session model
|
|
247
|
+
config: { provider: opencode-go, model: deepseek-v4.1-flash }
|
|
248
|
+
- id: music
|
|
249
|
+
config:
|
|
250
|
+
dj: # the DJ's own model, when you want a different one
|
|
251
|
+
provider: opencode-go
|
|
252
|
+
model: minimax-m3
|
|
253
|
+
```
|
|
206
254
|
|
|
207
255
|
Three things worth knowing:
|
|
208
256
|
|
|
209
257
|
- **`config` is replaced, not merged.** The loader assigns the whole object
|
|
210
258
|
(`entry.ts`: `this.options.config = value`), so if you also want, say,
|
|
211
259
|
`audioLevel`, keep every option in one block. Schema defaults fill the rest.
|
|
212
|
-
- **Discovery
|
|
213
|
-
|
|
214
|
-
(`listProviders()`),
|
|
215
|
-
entry. On this machine `opencode-go`'s catalogue is
|
|
260
|
+
- **Discovery is the last resort, and its choice is arbitrary.** Only when
|
|
261
|
+
neither a pin nor `agent-default-model` exists does the DJ ask the mounted LLM
|
|
262
|
+
service for its routes (`listProviders()`), take the first, then take that
|
|
263
|
+
route's first catalogue entry. On this machine `opencode-go`'s catalogue is
|
|
216
264
|
`minimax-m3, deepseek-v4-flash, gpt-5.6-luna`, so discovery curates with
|
|
217
|
-
**MiniMax-M3** — not a recommendation, just the first row.
|
|
218
|
-
is the only way to know which model is curating.
|
|
265
|
+
**MiniMax-M3** — not a recommendation, just the first row.
|
|
219
266
|
- **Partial pins are honoured.** A `provider` alone keeps that route and picks
|
|
220
267
|
its first catalogue model; a `model` alone keeps that model on the first
|
|
221
268
|
route.
|
|
@@ -224,7 +271,7 @@ Confirm it took effect:
|
|
|
224
271
|
|
|
225
272
|
| Where | What to look for |
|
|
226
273
|
|---|---|
|
|
227
|
-
| `/music/health` | `dj.model` is your pinned pair; `dj.modelSource` is `panel`, `config` or `discovered`; `dj.lastRoute` is the route that
|
|
274
|
+
| `/music/health` | `dj.model` is your pinned pair; `dj.modelSource` is `panel`, `config`, `agent-default` or `discovered`; `dj.resolvedFrom` is what the last plan actually used (`pin`, `agent-default`, `discovered`); `dj.sessionModel` is the deployment's own model; `dj.lastRoute` is the route that curated the last batch; `dj.sessionId` is the identity every call carries; `dj.modelError` is `null` once the tier works |
|
|
228
275
|
| Player, DJ status line | **AI DJ** plus the route (heuristic runs read **AI DJ (heuristic)**, and a declined tier prints the reason) |
|
|
229
276
|
| **测试 Test** in the player | Answers "does this route work?" in one click, without touching the queue |
|
|
230
277
|
| `music_dj` tool result | `AI DJ is on (model via opencode-go/deepseek-v4.1-flash)` |
|
|
@@ -237,6 +284,29 @@ the provider's own failure (a `LlmError` code such as `NO_ADAPTER`,
|
|
|
237
284
|
`MISSING_CREDENTIAL`, `AUTH`, `RATE_LIMIT`). The heuristic tier covers that batch
|
|
238
285
|
either way.
|
|
239
286
|
|
|
287
|
+
**If a provider says your usage does not follow its conventions**, the first
|
|
288
|
+
thing to check is the route's identity, not the request: pi-ai adds
|
|
289
|
+
`x-opencode-session` only for its own catalog routes `opencode-go` and
|
|
290
|
+
`opencode`. A hand-declared route id that merely points at
|
|
291
|
+
`https://opencode.ai/zen/go/v1` gets no wrapper and therefore no header, with
|
|
292
|
+
nothing in the logs to say so — keep the catalog route name. For the same
|
|
293
|
+
reason, do not set `x-opencode-session` in a profile's `headers`: the adapter
|
|
294
|
+
lets a configured value win over the generated one, which would freeze a single
|
|
295
|
+
id across every conversation. What remains after that is entitlement rather
|
|
296
|
+
than convention — a plan that only permits its own client's traffic cannot be
|
|
297
|
+
satisfied by a header, only by delegating transport to that client.
|
|
298
|
+
|
|
299
|
+
To see the bytes rather than trust the reasoning, run the bundled probe, point a
|
|
300
|
+
route's `baseURL` at it for one plan, and read what actually leaves the process:
|
|
301
|
+
|
|
302
|
+
```powershell
|
|
303
|
+
npm run probe:headers # http://127.0.0.1:8787 -> https://opencode.ai
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
You should see `x-opencode-session` carrying the DJ's id, and
|
|
307
|
+
`user-agent: deepseek-harness/<version> (+…)` — attribution that must never be
|
|
308
|
+
replaced by a provider-shaped one. Take the `baseURL` back out afterwards.
|
|
309
|
+
|
|
240
310
|
Editing `config` recomposes the running host, but **the plugin module itself is
|
|
241
311
|
cached** — restart the harness for the new route to take effect. If the model
|
|
242
312
|
tier fails for any reason (no adapter, bad credentials, unparseable reply,
|
|
@@ -263,6 +333,7 @@ add `config` to the inserted entry:
|
|
|
263
333
|
dj:
|
|
264
334
|
provider: opencode-go # enables the model tier
|
|
265
335
|
model: deepseek-v4.1-flash
|
|
336
|
+
sessionId: music-dj-mine # optional: pin the provider-visible identity
|
|
266
337
|
```
|
|
267
338
|
|
|
268
339
|
| Field | Default | Meaning |
|
|
@@ -271,7 +342,8 @@ add `config` to the inserted entry:
|
|
|
271
342
|
| `dataDir` | `$DSH_HOME/music` | Where `session.json` (cookie, history, feedback, settings) lives. |
|
|
272
343
|
| `audioLevel` | `exhigh` | Initial streaming quality, until the player's picker records a choice. One of the levels above; an unknown value falls back to `exhigh`. |
|
|
273
344
|
| `requestTimeoutMs` | `15000` | Per-request deadline for NetEase calls. |
|
|
274
|
-
| `dj.provider` / `dj.model` | unset | Model route for the DJ's model tier — see [Choosing the model](#choosing-the-model). Unset, the tier
|
|
345
|
+
| `dj.provider` / `dj.model` | unset | Model route for the DJ's model tier — see [Choosing the model](#choosing-the-model). Unset, the tier follows `agent-default-model` and only then falls back to discovery; any failure falls back to heuristics. |
|
|
346
|
+
| `dj.sessionId` | minted, persisted | The identity every model call carries. Adapters map it onto the provider's per-conversation header. Pin it to control what a provider sees, or leave it unset and let the DJ mint one per install. |
|
|
275
347
|
|
|
276
348
|
Player preferences (quality, DJ on/off, mood brief, batch size, extend
|
|
277
349
|
threshold) are persisted in `session.json` and edited from the panel.
|
|
@@ -397,7 +469,7 @@ down first. A restart brings both halves back into agreement.
|
|
|
397
469
|
|
|
398
470
|
```powershell
|
|
399
471
|
npm run check # node --check on every module
|
|
400
|
-
npm test #
|
|
472
|
+
npm test # 76 deterministic tests: pure, DJ, browser half
|
|
401
473
|
npm run test:live # 23 integration tests against the live NetEase API
|
|
402
474
|
npm run test:all # both
|
|
403
475
|
```
|
|
@@ -414,16 +486,17 @@ network cases inside it.
|
|
|
414
486
|
Or run one file directly:
|
|
415
487
|
|
|
416
488
|
```powershell
|
|
417
|
-
node test/netease.test.mjs #
|
|
418
|
-
node test/dj.test.mjs #
|
|
419
|
-
node test/client.test.mjs #
|
|
489
|
+
node test/netease.test.mjs # 18 pure: normalisation, quality ladder, cookies, player state
|
|
490
|
+
node test/dj.test.mjs # 37 AI DJ: model call identity, route resolution, failure reporting, queue invariants
|
|
491
|
+
node test/client.test.mjs # 21 browser half: the engine against a fake DOM, and the page it pairs with
|
|
420
492
|
node test/host.test.mjs # 23 integration: routes, streaming, curation, quality
|
|
421
493
|
```
|
|
422
494
|
|
|
423
|
-
The DJ tests drive
|
|
424
|
-
|
|
425
|
-
(`type`/`kind`), index filtering,
|
|
426
|
-
|
|
495
|
+
The DJ tests drive `ctx.llm.stream()` with a stub that emits the documented
|
|
496
|
+
chunks, and stub `ctx.agentDefaultModel` for route inheritance. So the
|
|
497
|
+
pinned-route happy path, both chunk spellings (`type`/`kind`), index filtering,
|
|
498
|
+
prose-wrapped JSON, the session identity every call must carry, inheritance from
|
|
499
|
+
the session model, discovery, and every fallback are covered without a provider.
|
|
427
500
|
|
|
428
501
|
The integration tests mount the plugin against stand-in `tools`/`webServer`
|
|
429
502
|
services and drive the captured route over a real `node:http` server, so they
|
|
@@ -439,6 +512,16 @@ component ever rendered**, so playback cannot depend on the Music page being
|
|
|
439
512
|
mounted. They also cover the seek handshake, failure reporting, disposal, and
|
|
440
513
|
the contract handshake.
|
|
441
514
|
|
|
515
|
+
The same fake DOM boots `lib/panel.html` itself, with an engine that reports
|
|
516
|
+
`playback: true` — the shell document owns the audio element. That is the case
|
|
517
|
+
where the page must fetch everything it renders on its own: lyrics used to be
|
|
518
|
+
requested only from the local fallback transport's `applySource`, so with the
|
|
519
|
+
engine playing the pane stayed empty for every track. Three tests hold the
|
|
520
|
+
line: the rendered track is asked for, one fetch per track rather than one per
|
|
521
|
+
poll, and a track change replaces the pane. Two more cover the pane's shape: it
|
|
522
|
+
is capped to a few lines, collapses to its header on demand, and remembers that
|
|
523
|
+
choice across loads — while still fetching the lines, so expanding is instant.
|
|
524
|
+
|
|
442
525
|
`npm test` runs the three deterministic files in sequence (`npm run test:all`
|
|
443
526
|
adds the live one), deliberately **not** `node --test <dir>`: the directory form forks one child process per file, which
|
|
444
527
|
is blocked in sandboxed environments.
|
|
@@ -462,6 +545,35 @@ is blocked in sandboxed environments.
|
|
|
462
545
|
[qrcode-generator](https://github.com/kazuhikoarase/qrcode-generator) by
|
|
463
546
|
Kazuhiko Arase, vendored so QR sign-in needs no network call or build step.
|
|
464
547
|
|
|
548
|
+
### Releasing
|
|
549
|
+
|
|
550
|
+
Releases are cut from GitHub and published by the `publish` workflow with **npm
|
|
551
|
+
trusted publishing (OIDC)**. There is no `NPM_TOKEN` secret and no
|
|
552
|
+
`NODE_AUTH_TOKEN` in the repository, and adding one would disable the OIDC
|
|
553
|
+
exchange and break publishing.
|
|
554
|
+
|
|
555
|
+
1. Bump `version` in `package.json`, commit, and push.
|
|
556
|
+
2. Create a GitHub release whose tag is `v<version>` — `v0.1.2` for `0.1.2` —
|
|
557
|
+
pointing at that commit.
|
|
558
|
+
|
|
559
|
+
The workflow runs `npm run check`, the deterministic suites, asserts the tag
|
|
560
|
+
matches `package.json`, and publishes. A tag push alone publishes nothing: the
|
|
561
|
+
trigger is `release: published`.
|
|
562
|
+
|
|
563
|
+
Two timings look like failures and are not:
|
|
564
|
+
|
|
565
|
+
- **A green job is not yet a live version.** The registry records a
|
|
566
|
+
`0.0.0-stage` placeholder first and reports the package as *being processed*;
|
|
567
|
+
the version becomes installable roughly a minute later. Inside that window the
|
|
568
|
+
packument still answers `404`, and publishing the same version from a second
|
|
569
|
+
place is refused with `409 Cannot publish over previously staged version`.
|
|
570
|
+
- **Registry reads can lag the publish**, so an `npm view` run immediately
|
|
571
|
+
afterwards may still say `404`. Wait a minute, or read it from CI instead.
|
|
572
|
+
|
|
573
|
+
Versions published this way carry a SLSA provenance attestation naming the
|
|
574
|
+
workflow, tag, and commit — `npm view @doitian/dsh-music@<version> dist.attestations`.
|
|
575
|
+
`0.1.0` predates the trusted publisher: it was published by hand and has none.
|
|
576
|
+
|
|
465
577
|
## Known limitations
|
|
466
578
|
|
|
467
579
|
- **Anonymous sessions cannot play VIP tracks**; NetEase returns `url: null`.
|
|
@@ -474,10 +586,16 @@ Kazuhiko Arase, vendored so QR sign-in needs no network call or build step.
|
|
|
474
586
|
fed to the DJ instead.
|
|
475
587
|
- **`apiPrefix` must stay `music`** unless `BASE` in `lib/client.js` is changed
|
|
476
588
|
to match.
|
|
477
|
-
- **The DJ's model tier is covered against
|
|
478
|
-
provider** —
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
589
|
+
- **The DJ's model tier is covered against stub contracts, not a live
|
|
590
|
+
provider** — request building, the session identity, route resolution, both
|
|
591
|
+
chunk spellings, index filtering and every fallback are tested, but a real
|
|
592
|
+
end-to-end model call has not been observed here. The heuristic tier is what
|
|
593
|
+
the live runs exercised.
|
|
594
|
+
- **The identity only becomes a header on the catalog routes that define one.**
|
|
595
|
+
pi-ai adds `x-opencode-session` for its own `opencode-go`/`opencode` routes. A
|
|
596
|
+
hand-declared route id pointing at the same endpoint gets no wrapper, so the
|
|
597
|
+
header is simply absent — keep the catalog route name, and never pin
|
|
598
|
+
`x-opencode-session` in a profile's `headers` (a configured value wins over
|
|
599
|
+
the generated one and freezes a single id for every conversation).
|
|
482
600
|
- **Chromium autoplay policy** may block playback the agent starts before the
|
|
483
601
|
user has interacted with the page; the panel then shows a *click to play* hint.
|
package/cordis.patch.yml
CHANGED
|
@@ -13,6 +13,20 @@
|
|
|
13
13
|
# requestTimeoutMs NetEase request deadline (default 15000).
|
|
14
14
|
# dj.provider model route for the AI DJ's model tier, e.g. opencode-go.
|
|
15
15
|
# dj.model exact model id, e.g. deepseek-v4.1-flash.
|
|
16
|
+
# dj.sessionId the identity every model call carries. A leaf call cannot
|
|
17
|
+
# set headers, so this string is the only identity the DJ
|
|
18
|
+
# can give a provider — and each adapter maps it onto that
|
|
19
|
+
# provider's own per-conversation header (pi-ai emits
|
|
20
|
+
# `x-opencode-session` for the `opencode-go` route). Left
|
|
21
|
+
# unset, one is minted on first use and persisted in
|
|
22
|
+
# session.json, so every plan and restart stays inside one
|
|
23
|
+
# conversation.
|
|
24
|
+
#
|
|
25
|
+
# Route resolution, highest first: the player's picker -> `dj.provider`/`dj.model`
|
|
26
|
+
# -> the deployment's own `agent-default-model` (the model the agent loop runs
|
|
27
|
+
# on, which is why "use my session model" needs no configuration) -> discovery,
|
|
28
|
+
# whose first row is arbitrary. The DJ is not an agent and starts none: each
|
|
29
|
+
# plan is one stateless leaf call, so there is no session to rotate or dispose.
|
|
16
30
|
#
|
|
17
31
|
# Without `dj.provider`/`dj.model` the DJ still works: it falls back to scoring
|
|
18
32
|
# similar songs, the daily recommendations and the charts by the listener's
|
package/lib/client.js
CHANGED
|
@@ -43,8 +43,34 @@ window.__ModuleLoader__.load({
|
|
|
43
43
|
const API = `${BASE}/api`;
|
|
44
44
|
/** Shared by the sidebar entry and the main panel it opens. */
|
|
45
45
|
const PANEL_ID = 'music';
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The locale namespace and its dictionary, registered with the host's
|
|
49
|
+
* `locale` service so the UI follows the Settings language. The service
|
|
50
|
+
* requires both shipped locales up front.
|
|
51
|
+
*/
|
|
52
|
+
const NS = 'music';
|
|
53
|
+
const DICTIONARY = {
|
|
54
|
+
zh: {
|
|
55
|
+
panel: '音乐',
|
|
56
|
+
iframeTitle: '网易云音乐播放器',
|
|
57
|
+
unreachableTitle: '无法连接到音乐插件',
|
|
58
|
+
unreachableBody: '音乐插件路由没有响应。请确认插件已在当前 profile 中启用,然后',
|
|
59
|
+
unreachableLink: '直接打开播放器',
|
|
60
|
+
unreachableSuffix: '。',
|
|
61
|
+
},
|
|
62
|
+
en: {
|
|
63
|
+
panel: 'Music',
|
|
64
|
+
iframeTitle: 'NetEase Cloud Music player',
|
|
65
|
+
unreachableTitle: 'Music is not reachable',
|
|
66
|
+
unreachableBody:
|
|
67
|
+
'The music plugin route did not answer. Check that the plugin is enabled in this profile, then ',
|
|
68
|
+
unreachableLink: 'open the player directly',
|
|
69
|
+
unreachableSuffix: '.',
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
/** Bound in apply() once the dictionary is registered; English until then. */
|
|
73
|
+
let t = (key) => DICTIONARY.en[key] ?? key;
|
|
48
74
|
|
|
49
75
|
/** How often the engine re-reads the host state; bounds control latency. */
|
|
50
76
|
const POLL_MS = 500;
|
|
@@ -53,7 +79,7 @@ window.__ModuleLoader__.load({
|
|
|
53
79
|
const IDLE_REPORT_MS = 5000;
|
|
54
80
|
|
|
55
81
|
/** Services required before the slot registrations can be made. */
|
|
56
|
-
const inject = ['slots'];
|
|
82
|
+
const inject = ['slots', 'locale'];
|
|
57
83
|
|
|
58
84
|
/** Absolute URL helper; the shell page and the panel share one origin. */
|
|
59
85
|
function origin() {
|
|
@@ -357,13 +383,13 @@ window.__ModuleLoader__.load({
|
|
|
357
383
|
opacity: 0.75,
|
|
358
384
|
},
|
|
359
385
|
},
|
|
360
|
-
h('div', { style: { fontWeight: 650, marginBottom: 6 } }, '
|
|
386
|
+
h('div', { style: { fontWeight: 650, marginBottom: 6 } }, t('unreachableTitle')),
|
|
361
387
|
h(
|
|
362
388
|
'div',
|
|
363
389
|
null,
|
|
364
|
-
'
|
|
365
|
-
h('a', { href: panelUrl(), target: '_blank', rel: 'noreferrer' }, '
|
|
366
|
-
'
|
|
390
|
+
t('unreachableBody'),
|
|
391
|
+
h('a', { href: panelUrl(), target: '_blank', rel: 'noreferrer' }, t('unreachableLink')),
|
|
392
|
+
t('unreachableSuffix'),
|
|
367
393
|
),
|
|
368
394
|
);
|
|
369
395
|
}
|
|
@@ -390,7 +416,7 @@ window.__ModuleLoader__.load({
|
|
|
390
416
|
|
|
391
417
|
return h('iframe', {
|
|
392
418
|
src: panelUrl(),
|
|
393
|
-
title: '
|
|
419
|
+
title: t('iframeTitle'),
|
|
394
420
|
allow: 'autoplay; clipboard-write; encrypted-media',
|
|
395
421
|
style: {
|
|
396
422
|
width: '100%',
|
|
@@ -425,13 +451,19 @@ window.__ModuleLoader__.load({
|
|
|
425
451
|
);
|
|
426
452
|
}
|
|
427
453
|
|
|
428
|
-
/**
|
|
454
|
+
/**
|
|
455
|
+
* Register the sidebar entry and the page it opens.
|
|
456
|
+
*
|
|
457
|
+
* Both registrations carry `locale: NS` and resolve their text through
|
|
458
|
+
* `t` at render time, so the slot system re-renders them in place when
|
|
459
|
+
* the user switches the Harness language — no remount, no refresh.
|
|
460
|
+
*/
|
|
429
461
|
function registerUi(ctx) {
|
|
430
462
|
ctx.slots.inject('main', () =>
|
|
431
|
-
ctx.slots.register({ name: 'main', key: PANEL_ID }, MusicPanel),
|
|
463
|
+
ctx.slots.register({ name: 'main', key: PANEL_ID, locale: NS }, MusicPanel),
|
|
432
464
|
);
|
|
433
465
|
ctx.slots.inject('sidebar.panellist', () =>
|
|
434
|
-
ctx.slots.register({ name: 'sidebar.panellist', id: PANEL_ID, order: 20, label:
|
|
466
|
+
ctx.slots.register({ name: 'sidebar.panellist', id: PANEL_ID, order: 20, locale: NS, label: () => t('panel') }, MusicIcon),
|
|
435
467
|
);
|
|
436
468
|
}
|
|
437
469
|
|
|
@@ -447,6 +479,10 @@ window.__ModuleLoader__.load({
|
|
|
447
479
|
* @returns disposer stopping playback and withdrawing both registrations.
|
|
448
480
|
*/
|
|
449
481
|
async function apply(ctx) {
|
|
482
|
+
// The dictionary goes in before anything renders, and bound `t` reads
|
|
483
|
+
// the live snapshot, so a later language switch needs no rewiring.
|
|
484
|
+
const disposeLocale = ctx.locale.register(NS, DICTIONARY);
|
|
485
|
+
t = ctx.locale.bind(NS);
|
|
450
486
|
const ui = ctx.inject(['slots'], () => registerUi(ctx));
|
|
451
487
|
const engine = await createEngine();
|
|
452
488
|
// The panel is same-origin, so it can nudge the engine after a command.
|
|
@@ -454,6 +490,7 @@ window.__ModuleLoader__.load({
|
|
|
454
490
|
return () => {
|
|
455
491
|
ui?.dispose?.();
|
|
456
492
|
engine.dispose();
|
|
493
|
+
disposeLocale?.();
|
|
457
494
|
if (window.__dshMusicEngine === engine) delete window.__dshMusicEngine;
|
|
458
495
|
};
|
|
459
496
|
}
|