dlinkclaw-openclaw 2026.8.24-alpha.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.
Files changed (77) hide show
  1. package/README.en.md +46 -0
  2. package/README.md +25 -0
  3. package/dist/index.d.ts +9 -0
  4. package/dist/index.js +1 -0
  5. package/dist/package.json +95 -0
  6. package/dist/src/account.d.ts +53 -0
  7. package/dist/src/account.js +394 -0
  8. package/dist/src/channel.d.ts +17 -0
  9. package/dist/src/channel.js +153 -0
  10. package/dist/src/client/client-manager.d.ts +4 -0
  11. package/dist/src/client/client-manager.js +10 -0
  12. package/dist/src/client/const.d.ts +37 -0
  13. package/dist/src/client/const.js +155 -0
  14. package/dist/src/client/crypto.d.ts +47 -0
  15. package/dist/src/client/crypto.js +260 -0
  16. package/dist/src/client/dl-client.d.ts +99 -0
  17. package/dist/src/client/dl-client.js +535 -0
  18. package/dist/src/client/dl-http.d.ts +27 -0
  19. package/dist/src/client/dl-http.js +223 -0
  20. package/dist/src/client/media-handler.d.ts +21 -0
  21. package/dist/src/client/media-handler.js +196 -0
  22. package/dist/src/client/message.d.ts +96 -0
  23. package/dist/src/client/message.js +1204 -0
  24. package/dist/src/client/timeout.d.ts +20 -0
  25. package/dist/src/client/timeout.js +36 -0
  26. package/dist/src/client/types.d.ts +224 -0
  27. package/dist/src/client/types.js +1 -0
  28. package/dist/src/monitor.d.ts +17 -0
  29. package/dist/src/monitor.js +320 -0
  30. package/dist/src/outbound.d.ts +4 -0
  31. package/dist/src/outbound.js +83 -0
  32. package/dist/src/policy.d.ts +42 -0
  33. package/dist/src/policy.js +205 -0
  34. package/dist/src/runtime.d.ts +9 -0
  35. package/dist/src/runtime.js +65 -0
  36. package/dist/src/services/index.d.ts +41 -0
  37. package/dist/src/services/index.js +225 -0
  38. package/dist/src/services/services.d.ts +152 -0
  39. package/dist/src/services/services.js +1066 -0
  40. package/dist/src/services/types.d.ts +391 -0
  41. package/dist/src/services/types.js +5 -0
  42. package/dist/src/services/ws-client.d.ts +174 -0
  43. package/dist/src/services/ws-client.js +866 -0
  44. package/dist/src/utils.d.ts +20 -0
  45. package/dist/src/utils.js +128 -0
  46. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  47. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  48. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  49. package/ext_capabilities/workrelay-speak/README.md +79 -0
  50. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  51. package/ext_capabilities/workrelay-speak/install.sh +280 -0
  52. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  53. package/ext_capabilities/workrelay-speak/manifest.json +49 -0
  54. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  55. package/ext_capabilities/workrelay-speak/status.sh +90 -0
  56. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  57. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  58. package/ext_capabilities/workrelay-speak-luxtts/install.sh +282 -0
  59. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +49 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/status.sh +94 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  67. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  68. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  69. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  70. package/ext_capabilities/workrelay-transcribe/install.sh +207 -0
  71. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  72. package/ext_capabilities/workrelay-transcribe/status.sh +62 -0
  73. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  74. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  75. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  76. package/openclaw.plugin.json +142 -0
  77. package/package.json +95 -0
@@ -0,0 +1,280 @@
1
+ #!/bin/bash
2
+ # WorkRelay 语音合成(TTS)一键安装脚本
3
+ # 通过 Docker 部署 Voicebox(jamiepine/voicebox),自动配置 OpenClaw
4
+ set -eo pipefail
5
+
6
+ RED='\033[0;31m'
7
+ GREEN='\033[0;32m'
8
+ YELLOW='\033[1;33m'
9
+ NC='\033[0m'
10
+
11
+ VB_DIR="$HOME/.workrelay/voicebox"
12
+ VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
13
+ VB_PORT="${WR_VOICEBOX_PORT:-17493}"
14
+ VB_DOCKER_PORT="${WR_VOICEBOX_DOCKER_PORT:-17600}"
15
+ # 2026-08-14:改为直接拉取阿里云公开镜像(已含 Kokoro 模型 + 英文修复补丁),
16
+ # 不再本地 clone + patch + build。镜像由石头哥推送到阿里云个人版仓库,公开仓库拉取免登录。
17
+ VB_IMAGE="${WR_VOICEBOX_IMAGE:-crpi-3zj44dlldca7vawm.cn-hangzhou.personal.cr.aliyuncs.com/shiduo/workrelay-tts:20260813}"
18
+ INSTALL_DIR="$(dirname "$(readlink -f "$0")")"
19
+
20
+ echo "============================================"
21
+ echo " WorkRelay 语音合成(TTS)- 一键安装"
22
+ echo "============================================"
23
+ echo ""
24
+
25
+ # ── 系统检查 ──
26
+ echo -n "[1/6] 检查系统环境... "
27
+
28
+ OS="$(uname -s)"
29
+ case "$OS" in
30
+ Linux) ;;
31
+ *)
32
+ echo -e "${RED}失败${NC}"
33
+ echo "错误:当前仅支持 Linux(TTS 需要 Docker)"
34
+ exit 1
35
+ ;;
36
+ esac
37
+
38
+ # 2026-08-11 修复:snap 安装的 docker 位于 /snap/bin,spawn 环境的 PATH 不含该目录,
39
+ # 导致 command -v docker 误判"未找到"。必须在 docker 检测之前补全 PATH。
40
+ export PATH="$PATH:/snap/bin:/usr/local/bin"
41
+
42
+ if ! command -v docker &>/dev/null; then
43
+ echo -e "${RED}失败${NC}"
44
+ echo "错误:未找到 Docker,请先安装 Docker"
45
+ echo " Ubuntu: sudo apt install docker.io docker-compose-v2"
46
+ exit 1
47
+ fi
48
+
49
+ if ! docker info &>/dev/null 2>&1; then
50
+ # 2026-08-18 增强:docker 权限多级自适应(尽量零 sudo,任何机器可用):
51
+ # ① 用户已在 docker 组(含会话未刷新)→ sg docker 切组执行
52
+ # ② 免密 sudo(sudo -n)可用 → sudo 包装
53
+ # ③ 都不行 → 给出一键修复指引,而不是干失败
54
+ if sg docker -c "docker info" &>/dev/null 2>&1; then
55
+ echo " ℹ 用户已加入 docker 组,通过 sg docker 执行(无需 sudo)"
56
+ docker() { sg docker -c "docker $(printf '%q ' "$@")"; }
57
+ elif sudo -n docker info &>/dev/null 2>&1; then
58
+ echo " ⚠ 当前用户无 docker 组权限,检测到免密 sudo,自动使用 sudo 执行 docker"
59
+ docker() { sudo docker "$@"; }
60
+ else
61
+ echo -e "${RED}失败${NC}"
62
+ echo "错误:Docker 未运行或无权限。请先执行一次以下命令:"
63
+ echo ""
64
+ echo " ① 启动 Docker:sudo systemctl start docker"
65
+ echo " ② 把当前用户加入 docker 组(避免以后每次要密码):"
66
+ echo " sudo usermod -aG docker \$USER"
67
+ echo " ③ 重启 openclaw 服务(或重新登录会话),然后重新安装"
68
+ echo ""
69
+ echo " 备选:为当前用户配置免密 sudo(sudo visudo 添加:\$USER ALL=(ALL) NOPASSWD: ALL)"
70
+ exit 1
71
+ fi
72
+ fi
73
+
74
+ echo -e "${GREEN}OK${NC} ($(docker --version | head -1), $(uname -o))"
75
+
76
+ # 2026-08-13 方案A:snap 版 docker 的 compose 插件与 docker CLI 客户端受 AppArmor 隔离,
77
+ # 读不了 $HOME 下的文件(compose 报 permission denied、build 报 open Dockerfile: no such file),
78
+ # 只有 dockerd daemon 放行 $HOME(所以 docker run 正常)。检测到 snap docker 时,
79
+ # 项目目录改用 /var/snap/docker/common/(AppArmor 明确放行 rw),compose/build/run 全通。
80
+ if [ -d /var/snap/docker/common ]; then
81
+ VB_DIR="/var/snap/docker/common/workrelay-voicebox"
82
+ echo " ℹ 检测到 snap docker,项目目录使用 $VB_DIR(AppArmor 放行读写)"
83
+ # /var/snap/docker/common 属 root,当前用户无写权限:需 root 预创建项目目录并移交给当前用户,
84
+ # 后续 docker pull / docker run 挂载的 output 目录写入不再需要提权。
85
+ # 2026-08-18:无免密 sudo 时给出手动命令指引(不依赖 sudo 密码)。
86
+ if [ -d "$VB_DIR" ]; then
87
+ sudo -n chown -R "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
88
+ elif ! sudo -n mkdir -p "$VB_DIR" 2>/dev/null; then
89
+ echo -e "${RED}失败${NC}"
90
+ echo "错误:无法创建项目目录 $VB_DIR(需要 root 权限)。"
91
+ echo "请手动执行一次以下命令后重新安装:"
92
+ echo " sudo mkdir -p $VB_DIR && sudo chown \$USER:\$USER $VB_DIR"
93
+ exit 1
94
+ fi
95
+ sudo -n chown "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
96
+ fi
97
+
98
+ # ── 磁盘空间 ──
99
+ echo -n "[2/6] 检查磁盘空间... "
100
+ # 2026-08-13:snap 模式下 VB_DIR 在 /var/snap/docker/common(可能与 $HOME 不同分区),
101
+ # 磁盘检查以实际项目目录所在分区为准
102
+ DF_TARGET="$VB_DIR"; [ -d "$DF_TARGET" ] || DF_TARGET="$HOME"
103
+ AVAIL=$(df -BM "$DF_TARGET" | tail -1 | awk '{print $4}' | sed 's/M//')
104
+ if [ "$AVAIL" -lt 10000 ]; then
105
+ echo -e "${RED}不足${NC}"
106
+ echo "错误:可用空间 ${AVAIL}MB,至少需要 10GB(Docker 镜像 + 模型 ~6GB)"
107
+ exit 1
108
+ fi
109
+ echo -e "${GREEN}OK${NC} (可用 ${AVAIL}MB)"
110
+
111
+ # ── 内存检查 ──
112
+ echo -n "[3/6] 检查内存... "
113
+ RAM=$(free -m | awk '/^Mem:/{print $2}')
114
+ if [ "$RAM" -lt 6000 ]; then
115
+ echo -e "${RED}不足 (${RAM}MB)${NC}"
116
+ echo "错误:至少需要 6GB 内存,当前仅 ${RAM}MB"
117
+ exit 1
118
+ fi
119
+ echo -e "${GREEN}OK${NC} (${RAM}MB)"
120
+
121
+ # ── 端口检查 ──
122
+ echo -n "[4/6] 检查端口 $VB_PORT... "
123
+ if ss -tlnp 2>/dev/null | grep -q ":$VB_PORT "; then
124
+ echo -e "${YELLOW}已被占用${NC}"
125
+ echo "提示:端口 $VB_PORT 已被占用,将使用 $VB_DOCKER_PORT"
126
+ VB_ACTUAL_PORT=$VB_DOCKER_PORT
127
+ else
128
+ echo -e "${GREEN}空闲${NC}"
129
+ VB_ACTUAL_PORT=$VB_PORT
130
+ fi
131
+
132
+ # ── 拉取 Voicebox 镜像 ──
133
+ echo -n "[5/6] 拉取 Voicebox 镜像... "
134
+ # 2026-08-14:镜像已由石头哥构建并推送到阿里云(含 Kokoro 模型 + 英文修复补丁),
135
+ # 直接 docker pull 即可,无需 clone 仓库 / patch / build。
136
+ mkdir -p "$VB_DIR"
137
+ if docker pull "$VB_IMAGE" > /tmp/wr-pull.log 2>&1; then
138
+ echo -e "${GREEN}OK${NC} ($(docker images --format '{{.Repository}}:{{.Tag}}' "$VB_IMAGE" | head -1))"
139
+ else
140
+ echo -e "${RED}失败${NC}"
141
+ tail -5 /tmp/wr-pull.log
142
+ echo "错误:拉取镜像失败,检查网络连接"
143
+ exit 1
144
+ fi
145
+
146
+ # ── 启动容器 ──
147
+ echo "[6/6] 启动 Voicebox 容器..."
148
+
149
+ # 创建输出目录(容器内 voicebox 用户需要可写)
150
+ mkdir -p "$VB_DIR/output"
151
+ chmod 777 "$VB_DIR/output" 2>/dev/null || true
152
+
153
+ # 2026-08-14:镜像已内置 Kokoro 模型(/home/voicebox/.cache/huggingface)且 ENV 已含
154
+ # HF_HUB_OFFLINE=1 / HF_HUB_CACHE,直接 docker run 即可。
155
+ # ⚠ 注意:不能挂空卷到 /home/voicebox/.cache/huggingface 或 /app/data,
156
+ # 否则会遮蔽镜像内已内置的模型/数据(旧版 clone+build 方案才需要挂卷)。
157
+ docker rm -f voicebox >/dev/null 2>&1 || true
158
+ if ! docker run -d --name voicebox --restart unless-stopped \
159
+ -p "127.0.0.1:${VB_ACTUAL_PORT}:17493" \
160
+ -v "$VB_DIR/output:/app/data/generations" \
161
+ -e LOG_LEVEL=info \
162
+ -e NUMBA_CACHE_DIR=/tmp/numba_cache \
163
+ --cpus 4 --memory 8g \
164
+ "$VB_IMAGE" >/dev/null 2>&1; then
165
+ echo -e "${RED}失败${NC}"
166
+ echo "错误:docker run 失败,执行 docker logs voicebox 查看日志"
167
+ exit 1
168
+ fi
169
+ echo " ✅ docker run 启动成功"
170
+
171
+ # 等待 API 就绪
172
+ echo -n " 等待 Voicebox API 就绪..."
173
+ for i in $(seq 1 90); do
174
+ if curl -s --max-time 2 "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" >/dev/null 2>&1; then
175
+ echo -e " ${GREEN}OK${NC} (耗时 ${i} 次检查)"
176
+ break
177
+ fi
178
+ sleep 5
179
+ echo -n "."
180
+ done
181
+
182
+ if ! curl -s --max-time 2 "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" >/dev/null 2>&1; then
183
+ echo ""
184
+ echo -e "${RED}Voicebox 启动失败${NC}"
185
+ echo " 检查日志: docker logs voicebox"
186
+ exit 1
187
+ fi
188
+
189
+ # ── 预设声音(安装后开箱即用)──
190
+ # 2026-08-14:模型已内置镜像,无需宿主机预下载/离线配置;只需创建预设声音 profile。
191
+ echo " ⚙ 配置预设声音..."
192
+
193
+ # 创建预设声音 profile(kokoro 中文女声 zf_xiaoxiao),id 写入 profile_id 供 workrelay-tts.sh 自动读取
194
+ mkdir -p "$HOME/.workrelay/speak"
195
+ PROFILE_FILE="$HOME/.workrelay/speak/profile_id"
196
+ # 先查已存在的同款 preset profile,避免重名 400
197
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 5 "http://${VB_HOST}:${VB_ACTUAL_PORT}/profiles" 2>/dev/null \
198
+ | python3 -c '
199
+ import json,sys
200
+ try:
201
+ d=json.load(sys.stdin)
202
+ for p in d:
203
+ if p.get("preset_engine")=="kokoro" and p.get("preset_voice_id")=="zf_xiaoxiao":
204
+ print(p.get("id","")); break
205
+ except Exception:
206
+ print("")
207
+ ' 2>/dev/null)
208
+ if [ -z "$EXIST_ID" ]; then
209
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 10 -X POST "http://${VB_HOST}:${VB_ACTUAL_PORT}/profiles" \
210
+ -H "Content-Type: application/json" \
211
+ -d '{"name":"WorkRelay 默认声音","description":"WorkRelay TTS 预设中文女声","language":"zh","voice_type":"preset","preset_engine":"kokoro","preset_voice_id":"zf_xiaoxiao"}' \
212
+ | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
213
+ fi
214
+ if [ -n "$EXIST_ID" ]; then
215
+ echo "$EXIST_ID" > "$PROFILE_FILE"
216
+ echo " ✅ 预设声音已就绪"
217
+ else
218
+ echo " ⚠ 预设声音创建失败,可手动设 WR_VOICEBOX_PROFILE_ID 指定声音"
219
+ fi
220
+
221
+ # ── 部署脚本 & 配置 OpenClaw ──
222
+ echo -n "[收尾] 部署 TTS 桥接脚本 & 配置 OpenClaw... "
223
+
224
+ mkdir -p "$HOME/.workrelay/speak"
225
+ cp "$INSTALL_DIR/workrelay-tts.sh" "$HOME/.workrelay/speak/"
226
+ chmod +x "$HOME/.workrelay/speak/workrelay-tts.sh"
227
+
228
+ # 2026-08-12:随安装部署说明文档(README + 声音克隆说明书 + 千问引擎说明书),用户安装后即可查看
229
+ cp "$INSTALL_DIR/README.md" "$HOME/.workrelay/speak/" 2>/dev/null || true
230
+ cp "$INSTALL_DIR/CLONE_GUIDE.md" "$HOME/.workrelay/speak/" 2>/dev/null || true
231
+ cp "$INSTALL_DIR/QWEN_GUIDE.md" "$HOME/.workrelay/speak/" 2>/dev/null || true
232
+
233
+ # 写入 OpenClaw TTS 配置
234
+ if command -v openclaw &>/dev/null; then
235
+ TTS_SCRIPT="$HOME/.workrelay/speak/workrelay-tts.sh"
236
+ openclaw config set messages.tts.auto off 2>/dev/null || true
237
+ openclaw config set messages.tts.provider tts-local-cli 2>/dev/null || true
238
+ openclaw config set messages.tts.providers.tts-local-cli.command "$TTS_SCRIPT" 2>/dev/null || true
239
+ openclaw config set messages.tts.providers.tts-local-cli.args '["{{Text}}","{{OutputPath}}"]' 2>/dev/null || true
240
+ openclaw config set messages.tts.providers.tts-local-cli.outputFormat wav 2>/dev/null || true
241
+ openclaw config set messages.tts.providers.tts-local-cli.timeoutMs 180000 2>/dev/null || true
242
+
243
+ # MCP 配置
244
+ openclaw config set mcp.servers.voicebox.url "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" 2>/dev/null || true
245
+ openclaw config set mcp.servers.voicebox.transport streamable-http 2>/dev/null || true
246
+ openclaw config set mcp.servers.voicebox.timeout 60 2>/dev/null || true
247
+
248
+ openclaw gateway restart 2>/dev/null || true
249
+ else
250
+ echo -e "${YELLOW}⚠ 未检测到 openclaw CLI,跳过自动配置${NC}"
251
+ fi
252
+
253
+ echo -e "${GREEN}OK${NC}"
254
+ echo ""
255
+
256
+ # ── 验证 ──
257
+ echo "============================================"
258
+ echo " 安装验证"
259
+ echo "============================================"
260
+
261
+ echo -n " Docker 容器: "
262
+ docker ps --format '{{.Names}}' 2>/dev/null | grep -q voicebox && echo -e "${GREEN}运行中${NC}" || echo -e "${RED}未运行${NC}"
263
+
264
+ echo -n " API 连通性: "
265
+ curl -s --max-time 2 "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" >/dev/null 2>&1 \
266
+ && echo -e "${GREEN}OK${NC} (http://${VB_HOST}:${VB_ACTUAL_PORT})" \
267
+ || echo -e "${RED}无法连接${NC}"
268
+
269
+ echo -n " TTS 桥接脚本: "
270
+ [ -f "$HOME/.workrelay/speak/workrelay-tts.sh" ] && echo -e "${GREEN}已部署${NC}" || echo -e "${RED}缺失${NC}"
271
+
272
+ echo ""
273
+ echo -e "${GREEN}✅ Voicebox TTS 安装完成!${NC}"
274
+ echo " API 地址: http://${VB_HOST}:${VB_ACTUAL_PORT}"
275
+ echo " 管理命令: docker ps | grep voicebox"
276
+ echo " 查看日志: docker logs -f voicebox"
277
+ echo ""
278
+ echo " 🎤 预设声音:开箱即用(Voicebox 内置 50+ 音色)"
279
+ echo " 🎙️ 声音克隆:可选,设 WR_VOICEBOX_PROFILE_ID=你的profile_id"
280
+ echo ""
@@ -0,0 +1,376 @@
1
+ #!/bin/bash
2
+ # WorkRelay 语音合成(TTS)一键安装脚本
3
+ # 通过 Docker 部署 Voicebox(jamiepine/voicebox),自动配置 OpenClaw
4
+ set -eo pipefail
5
+
6
+ RED='\033[0;31m'
7
+ GREEN='\033[0;32m'
8
+ YELLOW='\033[1;33m'
9
+ NC='\033[0m'
10
+
11
+ VB_DIR="$HOME/.workrelay/voicebox"
12
+ VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
13
+ VB_PORT="${WR_VOICEBOX_PORT:-17493}"
14
+ VB_DOCKER_PORT="${WR_VOICEBOX_DOCKER_PORT:-17600}"
15
+ INSTALL_DIR="$(dirname "$(readlink -f "$0")")"
16
+
17
+ echo "============================================"
18
+ echo " WorkRelay 语音合成(TTS)- 一键安装"
19
+ echo "============================================"
20
+ echo ""
21
+
22
+ # ── 系统检查 ──
23
+ echo -n "[1/7] 检查系统环境... "
24
+
25
+ OS="$(uname -s)"
26
+ case "$OS" in
27
+ Linux) ;;
28
+ *)
29
+ echo -e "${RED}失败${NC}"
30
+ echo "错误:当前仅支持 Linux(TTS 需要 Docker)"
31
+ exit 1
32
+ ;;
33
+ esac
34
+
35
+ # 2026-08-11 修复:snap 安装的 docker 位于 /snap/bin,spawn 环境的 PATH 不含该目录,
36
+ # 导致 command -v docker 误判"未找到"。必须在 docker 检测之前补全 PATH。
37
+ export PATH="$PATH:/snap/bin:/usr/local/bin"
38
+
39
+ if ! command -v docker &>/dev/null; then
40
+ echo -e "${RED}失败${NC}"
41
+ echo "错误:未找到 Docker,请先安装 Docker"
42
+ echo " Ubuntu: sudo apt install docker.io docker-compose-v2"
43
+ exit 1
44
+ fi
45
+
46
+ if ! docker info &>/dev/null 2>&1; then
47
+ # 2026-08-11 修复:docker 权限自适应。当前用户不在 docker 组时(docker.sock 属 root:root),
48
+ # 若检测到免密 sudo(sudo -n)可执行 docker,则用函数包装 docker 命令,脚本后续所有
49
+ # docker 调用(compose up / ps 等)自动走 sudo,无需用户手动加组/重新登录。
50
+ if sudo -n docker info &>/dev/null 2>&1; then
51
+ echo " ⚠ 当前用户无 docker 组权限,检测到免密 sudo,自动使用 sudo 执行 docker"
52
+ docker() { sudo docker "$@"; }
53
+ else
54
+ echo -e "${RED}失败${NC}"
55
+ echo "错误:Docker 未运行或无权限,请执行:"
56
+ echo " sudo systemctl start docker"
57
+ echo " sudo usermod -aG docker \$USER (需要重新登录)"
58
+ echo " 或为当前用户配置免密 sudo 后重试"
59
+ exit 1
60
+ fi
61
+ fi
62
+
63
+ echo -e "${GREEN}OK${NC} ($(docker --version | head -1), $(uname -o))"
64
+
65
+ # 2026-08-13 方案A:snap 版 docker 的 compose 插件与 docker CLI 客户端受 AppArmor 隔离,
66
+ # 读不了 $HOME 下的文件(compose 报 permission denied、build 报 open Dockerfile: no such file),
67
+ # 只有 dockerd daemon 放行 $HOME(所以 docker run 正常)。检测到 snap docker 时,
68
+ # 项目目录改用 /var/snap/docker/common/(AppArmor 明确放行 rw),compose/build/run 全通。
69
+ if [ -d /var/snap/docker/common ]; then
70
+ VB_DIR="/var/snap/docker/common/workrelay-voicebox"
71
+ echo " ℹ 检测到 snap docker,项目目录使用 $VB_DIR(AppArmor 放行读写)"
72
+ # /var/snap/docker/common 属 root,当前用户无写权限:需 sudo 预创建项目目录并移交给当前用户,
73
+ # 后续 git clone / compose 写文件(override/日志)不再需要提权
74
+ if [ -d "$VB_DIR" ]; then
75
+ sudo -n chown -R "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
76
+ elif ! sudo -n mkdir -p "$VB_DIR" 2>/dev/null; then
77
+ echo -e "${RED}失败${NC}"
78
+ echo "错误:无法创建 $VB_DIR(需要免密 sudo,或手动执行: sudo mkdir -p $VB_DIR && sudo chown \$USER:\$USER $VB_DIR)"
79
+ exit 1
80
+ fi
81
+ sudo -n chown "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
82
+ fi
83
+
84
+ # ── 磁盘空间 ──
85
+ echo -n "[2/7] 检查磁盘空间... "
86
+ # 2026-08-13:snap 模式下 VB_DIR 在 /var/snap/docker/common(可能与 $HOME 不同分区),
87
+ # 磁盘检查以实际项目目录所在分区为准
88
+ DF_TARGET="$VB_DIR"; [ -d "$DF_TARGET" ] || DF_TARGET="$HOME"
89
+ AVAIL=$(df -BM "$DF_TARGET" | tail -1 | awk '{print $4}' | sed 's/M//')
90
+ if [ "$AVAIL" -lt 10000 ]; then
91
+ echo -e "${RED}不足${NC}"
92
+ echo "错误:可用空间 ${AVAIL}MB,至少需要 10GB(Docker 镜像 + 模型 ~6GB)"
93
+ exit 1
94
+ fi
95
+ echo -e "${GREEN}OK${NC} (可用 ${AVAIL}MB)"
96
+
97
+ # ── 内存检查 ──
98
+ echo -n "[3/7] 检查内存... "
99
+ RAM=$(free -m | awk '/^Mem:/{print $2}')
100
+ if [ "$RAM" -lt 6000 ]; then
101
+ echo -e "${RED}不足 (${RAM}MB)${NC}"
102
+ echo "错误:至少需要 6GB 内存,当前仅 ${RAM}MB"
103
+ exit 1
104
+ fi
105
+ echo -e "${GREEN}OK${NC} (${RAM}MB)"
106
+
107
+ # ── 端口检查 ──
108
+ echo -n "[4/7] 检查端口 $VB_PORT... "
109
+ if ss -tlnp 2>/dev/null | grep -q ":$VB_PORT "; then
110
+ echo -e "${YELLOW}已被占用${NC}"
111
+ echo "提示:端口 $VB_PORT 已被占用,将使用 $VB_DOCKER_PORT"
112
+ VB_ACTUAL_PORT=$VB_DOCKER_PORT
113
+ else
114
+ echo -e "${GREEN}空闲${NC}"
115
+ VB_ACTUAL_PORT=$VB_PORT
116
+ fi
117
+
118
+ # ── 克隆 Voicebox 仓库 ──
119
+ echo -n "[5/7] 克隆 Voicebox 仓库... "
120
+ # 2026-08-13:snap 模式下 VB_DIR 可能已被 [1/7] 预创建为空目录,
121
+ # 空目录不能算"已存在",否则会跳过 clone 导致后面找不到 Dockerfile
122
+ if [ -d "$VB_DIR" ] && [ -n "$(ls -A "$VB_DIR" 2>/dev/null)" ]; then
123
+ echo "已存在,更新..."
124
+ cd "$VB_DIR" && git pull --quiet 2>/dev/null || true
125
+ else
126
+ mkdir -p "$VB_DIR"
127
+ git clone --quiet https://github.com/jamiepine/voicebox.git "$VB_DIR" 2>&1 || {
128
+ echo -e "${RED}失败${NC}"
129
+ echo "错误:克隆仓库失败,检查网络连接"
130
+ exit 1
131
+ }
132
+ fi
133
+ cd "$VB_DIR"
134
+
135
+ # 2026-08-13:Kokoro 中文 pipeline 英文朗读修复(英文被逐字母日式读出,如 Hello -> L.O.H.O.R.Y.O.)。
136
+ # clone 后自动 patch 源码,build 出的镜像自带修复——新用户启用 TTS 一步到位,无需手动操作。
137
+ if [ -f "$INSTALL_DIR/patch-kokoro-en.py" ]; then
138
+ python3 "$INSTALL_DIR/patch-kokoro-en.py" "$VB_DIR/backend/backends/kokoro_backend.py" \
139
+ || echo -e "${YELLOW} ⚠ Kokoro 英文修复 patch 失败(不影响安装,英文可能仍逐字母读)${NC}"
140
+ fi
141
+
142
+ echo -e "${GREEN}OK${NC}"
143
+
144
+ # ── 启动 Docker ──
145
+ echo "[6/7] 启动 Voicebox 服务(首次需拉取镜像+下载模型,约5-15分钟)..."
146
+
147
+ # 创建目录
148
+ mkdir -p "$VB_DIR/output"
149
+
150
+ # 2026-08-12 修复:
151
+ # 1) snap 版 docker 的 compose 插件受 AppArmor 隔离,无法读取 $HOME/.workrelay/ 下的
152
+ # compose 文件(报 "open ...: permission denied"),但 docker build/docker run 正常;
153
+ # 故 compose 失败时自动回退到 docker build + docker run。
154
+ # 2) 原 `2>&1 | tail -5` 会吞掉 compose 的退出码(set -e 不覆盖管道),脚本误以为成功、
155
+ # 空等 7.5 分钟 API;现在把完整日志写入文件并按真实退出码判断(配合顶部 set -o pipefail)。
156
+ COMPOSE_LOG="$VB_DIR/compose-up.log"
157
+ VB_MODE="compose" # compose | run(回退模式)
158
+ # 2026-08-13:-p voicebox 固定 compose 项目名——项目目录名变化(voicebox → workrelay-voicebox)
159
+ # 时卷名保持 voicebox_huggingface-cache 不变,可复用旧模型缓存卷
160
+ if docker compose -p voicebox up -d --build >"$COMPOSE_LOG" 2>&1; then
161
+ echo " ✅ docker compose 启动成功"
162
+ # compose 文件固定映射 127.0.0.1:17600:17493,API 检查必须用 17600
163
+ VB_ACTUAL_PORT=$VB_DOCKER_PORT
164
+ else
165
+ echo -e "${YELLOW} ⚠ docker compose 启动失败,回退到 docker build + docker run${NC}"
166
+ tail -15 "$COMPOSE_LOG"
167
+ VB_MODE="run"
168
+ docker rm -f voicebox >/dev/null 2>&1 || true
169
+ echo " 正在 docker build(首次需拉取基础镜像,约几分钟)..."
170
+ if ! docker build -t voicebox:local "$VB_DIR" >"$COMPOSE_LOG.build" 2>&1; then
171
+ echo -e "${RED}失败${NC}"
172
+ echo "错误:docker build 失败,日志见 $COMPOSE_LOG.build:"
173
+ tail -20 "$COMPOSE_LOG.build"
174
+ exit 1
175
+ fi
176
+ echo " 正在 docker run ..."
177
+ # 参数与 docker-compose.yml 保持一致(端口映射用 VB_ACTUAL_PORT,与 API 检查对齐)
178
+ if ! docker run -d --name voicebox --restart unless-stopped \
179
+ -p "127.0.0.1:${VB_ACTUAL_PORT}:17493" \
180
+ -v "$VB_DIR/output:/app/data/generations" \
181
+ -v voicebox-data:/app/data \
182
+ -v huggingface-cache:/home/voicebox/.cache/huggingface \
183
+ -e LOG_LEVEL=info \
184
+ -e NUMBA_CACHE_DIR=/tmp/numba_cache \
185
+ --cpus 4 --memory 8g \
186
+ voicebox:local >>"$COMPOSE_LOG" 2>&1; then
187
+ echo -e "${RED}失败${NC}"
188
+ echo "错误:docker run 失败,日志见 $COMPOSE_LOG:"
189
+ tail -20 "$COMPOSE_LOG"
190
+ exit 1
191
+ fi
192
+ echo " ✅ docker run 启动成功(回退模式)"
193
+ fi
194
+
195
+ # 等待 API 就绪
196
+ echo -n " 等待 Voicebox API 就绪..."
197
+ for i in $(seq 1 90); do
198
+ if curl -s --max-time 2 "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" >/dev/null 2>&1; then
199
+ echo -e " ${GREEN}OK${NC} (耗时 ${i} 次检查)"
200
+ break
201
+ fi
202
+ sleep 5
203
+ echo -n "."
204
+ done
205
+
206
+ if ! curl -s --max-time 2 "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" >/dev/null 2>&1; then
207
+ echo ""
208
+ echo -e "${RED}Voicebox 启动失败${NC}"
209
+ if [ "$VB_MODE" = "compose" ]; then
210
+ echo " 检查日志: docker compose -p voicebox -f $VB_DIR/docker-compose.yml logs voicebox"
211
+ else
212
+ echo " 检查日志: docker logs voicebox"
213
+ fi
214
+ exit 1
215
+ fi
216
+
217
+ # ── 2026-08-12:语音模型预下载 + 预设声音(安装后开箱即用)──
218
+ # 背景:容器内直连 huggingface.co 不通(DNS 被污染),hf-mirror 也未缓存相关模型,
219
+ # 因此模型必须在宿主机下载后放入 huggingface 缓存卷,并让容器离线加载(HF_HUB_OFFLINE=1)。
220
+ echo " ⚙ 准备语音模型与预设声音(首次下载约 1-3 分钟)..."
221
+
222
+ # 1) output 目录权限:容器内 voicebox 用户需要可写(宿主机目录属主不匹配)
223
+ chmod 777 "$VB_DIR/output" 2>/dev/null || true
224
+
225
+ # 2) 宿主机预下载 kokoro 模型(82M,含中文声音,~350MB 落盘)到缓存卷
226
+ if python3 -c "import huggingface_hub" >/dev/null 2>&1; then
227
+ HF_LOC=$(python3 -m pip show huggingface_hub 2>/dev/null | grep Location | awk '{print $2}')
228
+ # 2026-08-13:compose 模式卷名带项目前缀(voicebox_huggingface-cache),
229
+ # run 回退模式用裸卷名(huggingface-cache),双候选兜底
230
+ HF_VOL=$(docker volume inspect voicebox_huggingface-cache -f '{{.Mountpoint}}' 2>/dev/null)
231
+ [ -z "$HF_VOL" ] && HF_VOL=$(docker volume inspect huggingface-cache -f '{{.Mountpoint}}' 2>/dev/null)
232
+ if [ -n "$HF_VOL" ]; then
233
+ echo -n " 下载语音模型 (Kokoro-82M, ~350MB)... "
234
+ if sudo -n env PYTHONPATH="$HF_LOC" HF_HUB_DISABLE_PROGRESS_BARS=1 python3 -c "
235
+ from huggingface_hub import snapshot_download
236
+ snapshot_download('hexgrad/Kokoro-82M', cache_dir='$HF_VOL')
237
+ print('done')
238
+ " >/tmp/wr-model-dl.log 2>&1; then
239
+ echo -e "${GREEN}OK${NC}"
240
+ else
241
+ echo -e "${YELLOW}失败${NC}(详见 /tmp/wr-model-dl.log,可稍后手动下载)"
242
+ fi
243
+ else
244
+ echo " ⚠ 未找到 huggingface 缓存卷,跳过模型预下载(合成时可能联网超时)"
245
+ fi
246
+ else
247
+ echo " ⚠ 宿主机无 huggingface_hub,跳过模型预下载(可用 pip3 install huggingface_hub 后重跑)"
248
+ fi
249
+
250
+ # 3) 容器离线加载配置
251
+ # compose 模式:生成 override 文件(HF_HUB_CACHE 指向挂载的缓存卷 + 离线模式),重建容器生效
252
+ # run 模式:直接重建容器并注入环境变量
253
+ if [ "$VB_MODE" = "compose" ]; then
254
+ cat > "$VB_DIR/docker-compose.override.yml" <<'WR_EOF'
255
+ services:
256
+ voicebox:
257
+ environment:
258
+ - HF_HUB_CACHE=/home/voicebox/.cache/huggingface
259
+ - HF_HUB_OFFLINE=1
260
+ WR_EOF
261
+ docker compose -p voicebox up -d >/dev/null 2>&1 || true
262
+ else
263
+ docker rm -f voicebox >/dev/null 2>&1 || true
264
+ docker run -d --name voicebox --restart unless-stopped \
265
+ -p "127.0.0.1:${VB_ACTUAL_PORT}:17493" \
266
+ -v "$VB_DIR/output:/app/data/generations" \
267
+ -v voicebox-data:/app/data \
268
+ -v huggingface-cache:/home/voicebox/.cache/huggingface \
269
+ -e LOG_LEVEL=info \
270
+ -e NUMBA_CACHE_DIR=/tmp/numba_cache \
271
+ -e HF_HUB_CACHE=/home/voicebox/.cache/huggingface \
272
+ -e HF_HUB_OFFLINE=1 \
273
+ --cpus 4 --memory 8g \
274
+ voicebox:local >/dev/null 2>&1 || true
275
+ fi
276
+
277
+ # 重建后等 API 重新就绪(容器重启)
278
+ for i in $(seq 1 30); do
279
+ curl -s --max-time 2 "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" >/dev/null 2>&1 && break
280
+ sleep 3
281
+ echo -n "."
282
+ done
283
+
284
+ # 4) 创建预设声音 profile(kokoro 中文女声 zf_xiaoxiao),id 写入 profile_id 供 workrelay-tts.sh 自动读取
285
+ mkdir -p "$HOME/.workrelay/speak"
286
+ PROFILE_FILE="$HOME/.workrelay/speak/profile_id"
287
+ # 先查已存在的同款 preset profile,避免重名 400
288
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 5 "http://${VB_HOST}:${VB_ACTUAL_PORT}/profiles" 2>/dev/null \
289
+ | python3 -c '
290
+ import json,sys
291
+ try:
292
+ d=json.load(sys.stdin)
293
+ for p in d:
294
+ if p.get("preset_engine")=="kokoro" and p.get("preset_voice_id")=="zf_xiaoxiao":
295
+ print(p.get("id","")); break
296
+ except Exception:
297
+ print("")
298
+ ' 2>/dev/null)
299
+ if [ -z "$EXIST_ID" ]; then
300
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 10 -X POST "http://${VB_HOST}:${VB_ACTUAL_PORT}/profiles" \
301
+ -H "Content-Type: application/json" \
302
+ -d '{"name":"WorkRelay 默认声音","description":"WorkRelay TTS 预设中文女声","language":"zh","voice_type":"preset","preset_engine":"kokoro","preset_voice_id":"zf_xiaoxiao"}' \
303
+ | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
304
+ fi
305
+ if [ -n "$EXIST_ID" ]; then
306
+ echo "$EXIST_ID" > "$PROFILE_FILE"
307
+ echo " ✅ 预设声音已就绪"
308
+ else
309
+ echo " ⚠ 预设声音创建失败,可手动设 WR_VOICEBOX_PROFILE_ID 指定声音"
310
+ fi
311
+
312
+ # ── 部署脚本 & 配置 OpenClaw ──
313
+ echo -n "[7/7] 部署 TTS 桥接脚本 & 配置 OpenClaw... "
314
+
315
+ mkdir -p "$HOME/.workrelay/speak"
316
+ cp "$INSTALL_DIR/workrelay-tts.sh" "$HOME/.workrelay/speak/"
317
+ chmod +x "$HOME/.workrelay/speak/workrelay-tts.sh"
318
+
319
+ # 2026-08-12:随安装部署说明文档(README + 声音克隆说明书 + 千问引擎说明书),用户安装后即可查看
320
+ cp "$INSTALL_DIR/README.md" "$HOME/.workrelay/speak/" 2>/dev/null || true
321
+ cp "$INSTALL_DIR/CLONE_GUIDE.md" "$HOME/.workrelay/speak/" 2>/dev/null || true
322
+ cp "$INSTALL_DIR/QWEN_GUIDE.md" "$HOME/.workrelay/speak/" 2>/dev/null || true
323
+
324
+ # 写入 OpenClaw TTS 配置
325
+ if command -v openclaw &>/dev/null; then
326
+ TTS_SCRIPT="$HOME/.workrelay/speak/workrelay-tts.sh"
327
+ openclaw config set messages.tts.auto off 2>/dev/null || true
328
+ openclaw config set messages.tts.provider tts-local-cli 2>/dev/null || true
329
+ openclaw config set messages.tts.providers.tts-local-cli.command "$TTS_SCRIPT" 2>/dev/null || true
330
+ openclaw config set messages.tts.providers.tts-local-cli.args '["{{Text}}","{{OutputPath}}"]' 2>/dev/null || true
331
+ openclaw config set messages.tts.providers.tts-local-cli.outputFormat wav 2>/dev/null || true
332
+ openclaw config set messages.tts.providers.tts-local-cli.timeoutMs 180000 2>/dev/null || true
333
+
334
+ # MCP 配置
335
+ openclaw config set mcp.servers.voicebox.url "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" 2>/dev/null || true
336
+ openclaw config set mcp.servers.voicebox.transport streamable-http 2>/dev/null || true
337
+ openclaw config set mcp.servers.voicebox.timeout 60 2>/dev/null || true
338
+
339
+ openclaw gateway restart 2>/dev/null || true
340
+ else
341
+ echo -e "${YELLOW}⚠ 未检测到 openclaw CLI,跳过自动配置${NC}"
342
+ fi
343
+
344
+ echo -e "${GREEN}OK${NC}"
345
+ echo ""
346
+
347
+ # ── 验证 ──
348
+ echo "============================================"
349
+ echo " 安装验证"
350
+ echo "============================================"
351
+
352
+ echo -n " Docker 容器: "
353
+ docker ps --format '{{.Names}}' 2>/dev/null | grep -q voicebox && echo -e "${GREEN}运行中${NC}" || echo -e "${RED}未运行${NC}"
354
+
355
+ echo -n " API 连通性: "
356
+ curl -s --max-time 2 "http://${VB_HOST}:${VB_ACTUAL_PORT}/mcp" >/dev/null 2>&1 \
357
+ && echo -e "${GREEN}OK${NC} (http://${VB_HOST}:${VB_ACTUAL_PORT})" \
358
+ || echo -e "${RED}无法连接${NC}"
359
+
360
+ echo -n " TTS 桥接脚本: "
361
+ [ -f "$HOME/.workrelay/speak/workrelay-tts.sh" ] && echo -e "${GREEN}已部署${NC}" || echo -e "${RED}缺失${NC}"
362
+
363
+ echo ""
364
+ echo -e "${GREEN}✅ Voicebox TTS 安装完成!${NC}"
365
+ echo " API 地址: http://${VB_HOST}:${VB_ACTUAL_PORT}"
366
+ if [ "$VB_MODE" = "compose" ]; then
367
+ echo " 管理命令: cd $VB_DIR && docker compose -p voicebox ps"
368
+ echo " 查看日志: cd $VB_DIR && docker compose -p voicebox logs -f voicebox"
369
+ else
370
+ echo " 管理命令: docker ps | grep voicebox"
371
+ echo " 查看日志: docker logs -f voicebox"
372
+ fi
373
+ echo ""
374
+ echo " 🎤 预设声音:开箱即用(Voicebox 内置 50+ 音色)"
375
+ echo " 🎙️ 声音克隆:可选,设 WR_VOICEBOX_PROFILE_ID=你的profile_id"
376
+ echo ""