dsh-minimax-voice 0.2.0 → 0.2.1
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 +10 -0
- package/README.md +6 -0
- package/README.zh-CN.md +6 -0
- package/index.js +63 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1 — 2026-11-07
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`/mmvoice/api` now passes the host trust fence.** Every request goes through `connection.requestRejection()` (Host/Origin checks plus an authenticated browser session or API token) and is answered with `401`/`403` when it fails; if the `connection` service is unavailable, only loopback same-origin requests are accepted. Previously the local routes were unauthenticated, so a page on another origin could clone voices, synthesize audio or change the key.
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Marketplace badge in both READMEs, linking to the plugin's page in [awesome-dsh-plugin](https://awesome-dsh-plugin.com), the catalog behind the DSH plugin market.
|
|
12
|
+
|
|
3
13
|
## 0.2.0 — 2026-11-07
|
|
4
14
|
|
|
5
15
|
### Added
|
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/dsh-minimax-voice)
|
|
14
14
|
[](LICENSE)
|
|
15
|
+
[](https://awesome-dsh-plugin.com/p/Sunshine334419520/dsh-minimax-voice/)
|
|
15
16
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
16
17
|
[](https://nodejs.org/)
|
|
17
18
|
|
|
@@ -206,6 +207,10 @@ Check [MiniMax pricing](https://www.minimax.io/price) for current numbers. The p
|
|
|
206
207
|
- Audio and text are sent to MiniMax to do the work; nothing else leaves the machine.
|
|
207
208
|
- `remove()` deletes the local registry entry and preview only. Delete on MiniMax's side from the console if you also want the voice gone there.
|
|
208
209
|
|
|
210
|
+
## Local API security
|
|
211
|
+
|
|
212
|
+
`/mmvoice/api` can read the API key, write audio files and trigger billed calls, so every request passes the harness trust fence: [`connection.requestRejection()`](https://github.com/deepseek-ai/deepseek-harness) checks the Host/Origin pair (DNS rebinding) and requires an authenticated browser session or API token. A request that fails is answered with `401` or `403`. If the `connection` service is unavailable, only loopback, same-origin requests are accepted. Another website therefore cannot reach these routes to spend your MiniMax balance or swap the key.
|
|
213
|
+
|
|
209
214
|
## Troubleshooting
|
|
210
215
|
|
|
211
216
|
| Symptom | Fix |
|
|
@@ -213,6 +218,7 @@ Check [MiniMax pricing](https://www.minimax.io/price) for current numbers. The p
|
|
|
213
218
|
| `configured: false` | No key found; save one in the panel or export `MINIMAX_API_KEY` |
|
|
214
219
|
| Panel missing after install | Toggle the plugin off/on in **Plugins**, or restart the app |
|
|
215
220
|
| `mmvoice/api/status` returns 404 | Plugin is not loaded in this profile — check `dsh plugin --profile <name> list` |
|
|
221
|
+
| `mmvoice/api/*` returns 401 | No authenticated session for that request; open the panel from the harness UI (the routes are not open to other origins) |
|
|
216
222
|
| New voice shows `未激活` | Expected. It is never been synthesized yet, so the fee is not paid |
|
|
217
223
|
| Clone fails with 400 | Sample too short/long/quiet, or the account has no voice-cloning permission |
|
|
218
224
|
| Transcription returns empty text | Language hint conflicts with the audio, or the file is mostly music |
|
package/README.zh-CN.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
[](https://www.npmjs.com/package/dsh-minimax-voice)
|
|
14
14
|
[](LICENSE)
|
|
15
|
+
[](https://awesome-dsh-plugin.com/p/Sunshine334419520/dsh-minimax-voice/)
|
|
15
16
|
[](https://github.com/deepseek-ai/deepseek-harness)
|
|
16
17
|
[](https://nodejs.org/)
|
|
17
18
|
|
|
@@ -206,6 +207,10 @@ export function apply(ctx) {
|
|
|
206
207
|
- 音频和文本会发给 MiniMax 完成处理,其余数据不出本机。
|
|
207
208
|
- `remove()` 只删本地注册表和试听文件;要连账号侧一起删,请去 MiniMax 控制台。
|
|
208
209
|
|
|
210
|
+
## 本地接口安全
|
|
211
|
+
|
|
212
|
+
`/mmvoice/api` 能读 API Key、写音频文件、触发计费调用,所以每个请求都要过宿主的信任栅栏:[`connection.requestRejection()`](https://github.com/deepseek-ai/deepseek-harness) 会校验 Host/Origin(防 DNS 重绑定),并要求已认证的浏览器会话或 API 令牌;不通过就返回 `401` 或 `403`。拿不到 `connection` 服务时,只接受回环地址的同源请求。别的网页因此够不到这些接口,无法花你的 MiniMax 余额或换掉 Key。
|
|
213
|
+
|
|
209
214
|
## 常见问题
|
|
210
215
|
|
|
211
216
|
| 现象 | 处理 |
|
|
@@ -213,6 +218,7 @@ export function apply(ctx) {
|
|
|
213
218
|
| `configured: false` | 没找到 Key,去面板保存或导出 `MINIMAX_API_KEY` |
|
|
214
219
|
| 装完设置页没出现 | 在「插件」里关掉再打开,或重启应用 |
|
|
215
220
|
| `/mmvoice/api/status` 返回 404 | 这个 profile 没加载插件,`dsh plugin --profile <name> list` 看看 |
|
|
221
|
+
| `/mmvoice/api/*` 返回 401 | 该请求没有已认证的会话;从 harness 界面里打开面板使用(接口不对其他来源开放) |
|
|
216
222
|
| 新音色显示 `未激活` | 正常。还没合成过,所以那 9.9 元也没收 |
|
|
217
223
|
| 复刻报 400 | 样本太短 / 太长 / 太安静,或账号没开通复刻权限 |
|
|
218
224
|
| 转写结果为空 | 语种提示与音频不符,或者文件基本是纯音乐 |
|
package/index.js
CHANGED
|
@@ -32,6 +32,60 @@ const DEFAULT_PREVIEW_TEXT = '你好,这是复刻音色的试听效果,听
|
|
|
32
32
|
const MAX_BODY_BYTES = 24 * 1024 * 1024
|
|
33
33
|
const ROUTE_PREFIX = '/mmvoice/api'
|
|
34
34
|
|
|
35
|
+
/* ------------------------------------------------------------ trust fence */
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* `/mmvoice/api` 能读写 API Key、音色样本、生成的音频,并会触发计费调用,
|
|
39
|
+
* 所以它必须接宿主的浏览器认证,而不是"只监听回环所以安全"。
|
|
40
|
+
* dsh 的 connection 服务提供 `requestRejection({ headers }): 401 | 403 | undefined`,
|
|
41
|
+
* 它同时做 Host/Origin 校验(防 DNS 重绑定)和浏览器会话认证。
|
|
42
|
+
* 拿不到该服务时退化为只接受回环地址的同源请求(fail closed)。
|
|
43
|
+
*/
|
|
44
|
+
function serviceOf(ctx, name) {
|
|
45
|
+
try {
|
|
46
|
+
return ctx.get(name)
|
|
47
|
+
} catch {
|
|
48
|
+
return undefined
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function isLoopbackRequest(req) {
|
|
53
|
+
const host = String((req && req.headers && req.headers.host) || '').toLowerCase()
|
|
54
|
+
const address = String((req && req.socket && req.socket.remoteAddress) || '')
|
|
55
|
+
const hostOk = /^(127\.0\.0\.1|localhost|\[::1\])(:\d+)?$/.test(host)
|
|
56
|
+
const peerOk =
|
|
57
|
+
address === '' || address === '127.0.0.1' || address === '::1' || address === '::ffff:127.0.0.1'
|
|
58
|
+
return hostOk && peerOk
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function trustRejection(ctx, req) {
|
|
62
|
+
const connection = serviceOf(ctx, 'connection')
|
|
63
|
+
if (connection && typeof connection.requestRejection === 'function') {
|
|
64
|
+
try {
|
|
65
|
+
const code = connection.requestRejection(req)
|
|
66
|
+
if (code === undefined) return 0
|
|
67
|
+
return typeof code === 'number' ? code : 403
|
|
68
|
+
} catch {
|
|
69
|
+
return 403
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return isLoopbackRequest(req) ? 0 : 403
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function denyRequest(res, status) {
|
|
76
|
+
try {
|
|
77
|
+
const body = JSON.stringify({ error: status === 401 ? 'unauthorized' : 'forbidden' })
|
|
78
|
+
res.writeHead(status, {
|
|
79
|
+
'content-type': 'application/json; charset=utf-8',
|
|
80
|
+
'content-length': String(Buffer.byteLength(body)),
|
|
81
|
+
'cache-control': 'no-store',
|
|
82
|
+
})
|
|
83
|
+
res.end(body)
|
|
84
|
+
} catch {
|
|
85
|
+
/* the connection is already gone */
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
35
89
|
/* ------------------------------------------------------------------ utils */
|
|
36
90
|
|
|
37
91
|
function text(value) {
|
|
@@ -958,8 +1012,16 @@ export function apply(ctx, config = {}) {
|
|
|
958
1012
|
const claimRoute = (webServer) => {
|
|
959
1013
|
if (routeRegistered || !webServer) return
|
|
960
1014
|
routeRegistered = true
|
|
1015
|
+
const handler = (req, res) => {
|
|
1016
|
+
const rejection = trustRejection(ctx, req)
|
|
1017
|
+
if (rejection) {
|
|
1018
|
+
denyRequest(res, rejection)
|
|
1019
|
+
return
|
|
1020
|
+
}
|
|
1021
|
+
void voice.handleHttp(req, res)
|
|
1022
|
+
}
|
|
961
1023
|
ctx.effect(
|
|
962
|
-
() => webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler
|
|
1024
|
+
() => webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler }),
|
|
963
1025
|
'minimax-voice: http route',
|
|
964
1026
|
)
|
|
965
1027
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-minimax-voice",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Voice cloning, text-to-speech and speech-to-text for DeepSeek Harness, powered by MiniMax · DeepSeek Harness 的 MiniMax 音色复刻 / 文本转音频 / 语音转文字插件",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|