dlinkclaw-openclaw 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.
Files changed (85) hide show
  1. package/LICENSE +201 -0
  2. package/README.en.md +46 -0
  3. package/README.md +15 -0
  4. package/dist/index.d.ts +9 -0
  5. package/dist/index.js +1 -0
  6. package/dist/package.json +95 -0
  7. package/dist/src/account.d.ts +54 -0
  8. package/dist/src/account.js +394 -0
  9. package/dist/src/channel.d.ts +17 -0
  10. package/dist/src/channel.js +153 -0
  11. package/dist/src/client/client-manager.d.ts +4 -0
  12. package/dist/src/client/client-manager.js +10 -0
  13. package/dist/src/client/const.d.ts +37 -0
  14. package/dist/src/client/const.js +155 -0
  15. package/dist/src/client/crypto.d.ts +47 -0
  16. package/dist/src/client/crypto.js +260 -0
  17. package/dist/src/client/dl-client.d.ts +117 -0
  18. package/dist/src/client/dl-client.js +588 -0
  19. package/dist/src/client/dl-http.d.ts +27 -0
  20. package/dist/src/client/dl-http.js +223 -0
  21. package/dist/src/client/media-handler.d.ts +21 -0
  22. package/dist/src/client/media-handler.js +196 -0
  23. package/dist/src/client/message.d.ts +96 -0
  24. package/dist/src/client/message.js +1211 -0
  25. package/dist/src/client/timeout.d.ts +20 -0
  26. package/dist/src/client/timeout.js +36 -0
  27. package/dist/src/client/types.d.ts +226 -0
  28. package/dist/src/client/types.js +1 -0
  29. package/dist/src/monitor.d.ts +18 -0
  30. package/dist/src/monitor.js +322 -0
  31. package/dist/src/outbound.d.ts +6 -0
  32. package/dist/src/outbound.js +78 -0
  33. package/dist/src/policy.d.ts +42 -0
  34. package/dist/src/policy.js +216 -0
  35. package/dist/src/runtime.d.ts +9 -0
  36. package/dist/src/runtime.js +65 -0
  37. package/dist/src/services/gateway-rpc.d.ts +37 -0
  38. package/dist/src/services/gateway-rpc.js +37 -0
  39. package/dist/src/services/index.d.ts +41 -0
  40. package/dist/src/services/index.js +245 -0
  41. package/dist/src/services/services.d.ts +169 -0
  42. package/dist/src/services/services.js +1160 -0
  43. package/dist/src/services/types.d.ts +391 -0
  44. package/dist/src/services/types.js +5 -0
  45. package/dist/src/services/ws-client.d.ts +186 -0
  46. package/dist/src/services/ws-client.js +896 -0
  47. package/dist/src/utils.d.ts +20 -0
  48. package/dist/src/utils.js +128 -0
  49. package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
  50. package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
  51. package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
  52. package/ext_capabilities/workrelay-speak/README.md +79 -0
  53. package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
  54. package/ext_capabilities/workrelay-speak/install.sh +329 -0
  55. package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
  56. package/ext_capabilities/workrelay-speak/manifest.json +63 -0
  57. package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
  58. package/ext_capabilities/workrelay-speak/status.sh +111 -0
  59. package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
  60. package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
  61. package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
  62. package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
  63. package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
  64. package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
  65. package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
  66. package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
  67. package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
  68. package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
  69. package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
  70. package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
  71. package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
  72. package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
  73. package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
  74. package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
  75. package/ext_capabilities/workrelay-transcribe/README.md +76 -0
  76. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
  77. package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
  78. package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
  79. package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
  80. package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
  81. package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
  82. package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
  83. package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
  84. package/openclaw.plugin.json +142 -0
  85. package/package.json +95 -0
@@ -0,0 +1,115 @@
1
+ #!/bin/bash
2
+ # WorkRelay 语音合成(LuxTTS 克隆声)状态检查
3
+ # 返回值: 0=已启用, 1=未安装, 2=部分安装, 3=Docker未运行
4
+ # 同时输出 JSON 给客户端解析
5
+ # 2026-08-15 新增能力:独立容器 voicebox-luxtts + 端口 17601 + provider tts-local-cli-luxtts
6
+
7
+ set -e
8
+
9
+ STATUS_OK=0
10
+ STATUS_NOT_INSTALLED=1
11
+ STATUS_PARTIAL=2
12
+ STATUS_DOCKER_DOWN=3
13
+
14
+ VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
15
+ VB_PORT="${WR_VOICEBOX_LUXTTS_PORT:-17601}"
16
+ VB_DOCKER_PORT="${WR_VOICEBOX_LUXTTS_PORT:-17601}"
17
+
18
+ export PATH="$PATH:/snap/bin:/usr/local/bin"
19
+ # 2026-08-18:与 install.sh 保持一致的 docker 权限自适应(sg docker / 免密 sudo)
20
+ if ! docker info &>/dev/null 2>&1; then
21
+ if sg docker -c "docker info" &>/dev/null 2>&1; then
22
+ docker() { sg docker -c "docker $(printf '%q ' "$@")"; }
23
+ elif sudo -n docker info &>/dev/null 2>&1; then
24
+ docker() { sudo docker "$@"; }
25
+ fi
26
+ fi
27
+
28
+ # 检测实际端口(17601,fallback 同端口)
29
+ check_port=$VB_PORT
30
+ curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1 && check_port=$VB_PORT || {
31
+ curl -s --max-time 2 "http://${VB_HOST}:${VB_DOCKER_PORT}/mcp" >/dev/null 2>&1 && check_port=$VB_DOCKER_PORT
32
+ }
33
+
34
+ # 检查1: Docker
35
+ check_docker=0
36
+ docker info &>/dev/null 2>&1 && check_docker=1 || true
37
+
38
+ # 检查2: voicebox-luxtts 容器
39
+ check_container=0
40
+ docker ps --format '{{.Names}}' 2>/dev/null | grep -q "voicebox-luxtts" && check_container=1 || true
41
+
42
+ # 检查3: API
43
+ check_api=0
44
+ curl -s --max-time 3 "http://${VB_HOST}:${check_port}/mcp" >/dev/null 2>&1 && check_api=1 || true
45
+
46
+ # 检查4: TTS 桥接脚本
47
+ check_tts_script=0
48
+ [ -f "$HOME/.workrelay/speak-luxtts/workrelay-tts.sh" ] && check_tts_script=1 || true
49
+
50
+ # 检查5: 克隆声音 profile
51
+ check_profile=0
52
+ [ -f "$HOME/.workrelay/speak-luxtts/profile_id" ] && check_profile=1 || true
53
+
54
+ # 检查6: OpenClaw TTS 配置(2026-09-14 openclaw 升级后:config 路径 messages.tts.* → 顶层 tts.*;
55
+ # 不再调 openclaw CLI(升级后可能挂起卡插件队列),改为直接读 openclaw.json)
56
+ # 2026-09-14 互斥判定:仅当 tts.provider == tts-local-cli-luxtts 时才算"已启用"(TTS 只能启用一个)
57
+ check_config=0
58
+ check_provider=0
59
+ OC_JSON="${OPENCLAW_HOME:-$HOME/.openclaw}/openclaw.json"
60
+ if [ -f "$OC_JSON" ]; then
61
+ grep -q '"tts-local-cli-luxtts":' "$OC_JSON" && check_config=1 || true
62
+ CUR_PROVIDER=$(python3 -c '
63
+ import json,sys
64
+ try:
65
+ c=json.load(open(sys.argv[1]))
66
+ print((c.get("tts") or {}).get("provider",""))
67
+ except Exception:
68
+ print("")
69
+ ' "$OC_JSON" 2>/dev/null)
70
+ [ "$CUR_PROVIDER" = "tts-local-cli-luxtts" ] && check_provider=1 || true
71
+ fi
72
+
73
+ # ── 汇总状态 ──
74
+ total=$((check_docker + check_container + check_api + check_tts_script + check_profile + check_config))
75
+
76
+ case $total in
77
+ 6)
78
+ # 2026-09-14 互斥:安装齐全后再看 provider 是否指向本引擎
79
+ if [ "$check_provider" -eq 1 ]; then
80
+ status="enabled"; desc="语音合成(LuxTTS 克隆声)已启用"; exit_code=$STATUS_OK
81
+ else
82
+ status="disabled"; desc="已安装但未启用(当前语音合成为其它引擎,TTS 只能启用一个)"; exit_code=$STATUS_NOT_INSTALLED
83
+ fi
84
+ ;;
85
+ 0) status="disabled"; desc="未安装"; exit_code=$STATUS_NOT_INSTALLED ;;
86
+ *)
87
+ if [ "$check_container" -eq 0 ] && [ "$check_docker" -eq 1 ]; then
88
+ status="disabled"; desc="Docker 运行中但 voicebox-luxtts 未安装"; exit_code=$STATUS_NOT_INSTALLED
89
+ elif [ "$check_container" -eq 1 ] && [ "$check_api" -eq 0 ]; then
90
+ status="partial"; desc="voicebox-luxtts 容器存在但 API 未就绪(可能在下载模型)"; exit_code=$STATUS_DOCKER_DOWN
91
+ else
92
+ status="partial"; desc="部分安装"; exit_code=$STATUS_PARTIAL
93
+ fi
94
+ ;;
95
+ esac
96
+
97
+ # ── 输出 JSON ──
98
+ cat <<EOF
99
+ {
100
+ "status": "$status",
101
+ "description": "$desc",
102
+ "endpoint": "http://${VB_HOST}:${check_port}",
103
+ "checks": {
104
+ "docker": $( [ $check_docker -eq 1 ] && echo true || echo false ),
105
+ "container": $( [ $check_container -eq 1 ] && echo true || echo false ),
106
+ "api": $( [ $check_api -eq 1 ] && echo true || echo false ),
107
+ "tts_script": $( [ $check_tts_script -eq 1 ] && echo true || echo false ),
108
+ "profile": $( [ $check_profile -eq 1 ] && echo true || echo false ),
109
+ "openclaw_config": $( [ $check_config -eq 1 ] && echo true || echo false ),
110
+ "provider_active": $( [ $check_provider -eq 1 ] && echo true || echo false )
111
+ }
112
+ }
113
+ EOF
114
+
115
+ exit $exit_code
@@ -0,0 +1,98 @@
1
+ #!/bin/bash
2
+ # WorkRelay TTS 桥接脚本(LuxTTS 引擎)- 调用本地 Voicebox (voicebox-luxtts) REST API
3
+ # 环境变量:
4
+ # WR_VOICEBOX_HOST - Voicebox 服务地址(默认 127.0.0.1)
5
+ # WR_VOICEBOX_LUXTTS_PORT - LuxTTS 实例端口(默认 17601,独立于 Kokoro 版)
6
+ # WR_VOICEBOX_PROFILE_ID - 声音 profile ID(可选,缺省读取 ~/.workrelay/speak-luxtts/profile_id)
7
+ # WR_VOICEBOX_LANG - 语言(默认 zh)
8
+ # 固定引擎 luxtts(LuxTTS 为克隆声音引擎,无预设音色,必须使用克隆 profile)
9
+
10
+ set -e
11
+
12
+ TEXT="$1"
13
+ OUT="$2"
14
+
15
+ if [ -z "$TEXT" ] || [ -z "$OUT" ]; then
16
+ echo "usage: $0 <text> <output_path>" >&2
17
+ exit 1
18
+ fi
19
+
20
+ VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
21
+ VB_PORT="${WR_VOICEBOX_LUXTTS_PORT:-17601}"
22
+ PROFILE_ID="${WR_VOICEBOX_PROFILE_ID:-}"
23
+ ENGINE="luxtts"
24
+ LANG="${WR_VOICEBOX_LANG:-zh}"
25
+
26
+ BASE_URL="http://${VB_HOST}:${VB_PORT}"
27
+ if ! curl -s --max-time 2 "${BASE_URL}/mcp" >/dev/null 2>&1; then
28
+ echo "ERROR: Voicebox (LuxTTS) API 不可达 ($VB_HOST:$VB_PORT)" >&2
29
+ exit 1
30
+ fi
31
+
32
+ # ── profile 自动解析(install.sh 写入石头哥克隆声 profile id)──
33
+ if [ -z "$PROFILE_ID" ] && [ -f "$HOME/.workrelay/speak-luxtts/profile_id" ]; then
34
+ PROFILE_ID=$(cat "$HOME/.workrelay/speak-luxtts/profile_id" 2>/dev/null | tr -d ' \n')
35
+ fi
36
+ if [ -z "$PROFILE_ID" ]; then
37
+ # 兜底:从 /profiles 找克隆 profile(优先中文)
38
+ PROFILE_ID=$(curl --noproxy '*' -s --max-time 5 "$BASE_URL/profiles" 2>/dev/null \
39
+ | python3 -c '
40
+ import json,sys
41
+ out=""
42
+ try:
43
+ d=json.load(sys.stdin)
44
+ for p in d:
45
+ if p.get("voice_type")=="cloned" and p.get("language")=="zh":
46
+ out=p.get("id",""); break
47
+ if not out:
48
+ for p in d:
49
+ if p.get("voice_type")=="cloned":
50
+ out=p.get("id",""); break
51
+ print(out)
52
+ except Exception:
53
+ print("")
54
+ ' 2>/dev/null)
55
+ fi
56
+ if [ -z "$PROFILE_ID" ]; then
57
+ echo "ERROR: 未找到可用的克隆声音 profile(Voicebox /profiles 为空)" >&2
58
+ exit 1
59
+ fi
60
+
61
+ # JSON-escape text
62
+ TEXT_JSON=$(python3 -c 'import json,sys; print(json.dumps(sys.argv[1]))' "$TEXT")
63
+
64
+ # 提交生成请求(固定 engine=luxtts)
65
+ RESP=$(curl --noproxy '*' -s --max-time 15 -X POST "$BASE_URL/generate" \
66
+ -H "Content-Type: application/json" \
67
+ -H "X-Voicebox-Client-Id: workrelay-tts-luxtts" \
68
+ -d "{\"text\": $TEXT_JSON, \"profile_id\": \"$PROFILE_ID\", \"language\": \"$LANG\", \"engine\": \"$ENGINE\"}")
69
+
70
+ GEN_ID=$(echo "$RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
71
+ if [ -z "$GEN_ID" ]; then
72
+ echo "ERROR: Voicebox 生成提交失败: $RESP" >&2
73
+ exit 1
74
+ fi
75
+
76
+ # 轮询等待完成(CPU 推理约 10-180 秒,视文本长度)
77
+ MAX_POLLS=180
78
+ for i in $(seq 1 $MAX_POLLS); do
79
+ sleep 1
80
+ ST=$(curl --noproxy '*' -s --max-time 10 "$BASE_URL/history/$GEN_ID" 2>/dev/null \
81
+ | python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("status",""))' 2>/dev/null)
82
+ if [ "$ST" = "completed" ]; then
83
+ break
84
+ fi
85
+ if [ "$ST" = "error" ] || [ "$ST" = "failed" ]; then
86
+ echo "ERROR: Voicebox 生成失败 (id=$GEN_ID, status=$ST)" >&2
87
+ exit 1
88
+ fi
89
+ done
90
+
91
+ # 下载音频
92
+ curl --noproxy '*' -s --max-time 30 -o "$OUT" "$BASE_URL/audio/$GEN_ID"
93
+ if [ -s "$OUT" ]; then
94
+ exit 0
95
+ else
96
+ echo "ERROR: Voicebox 音频下载失败 (id=$GEN_ID)" >&2
97
+ exit 1
98
+ fi
@@ -0,0 +1,62 @@
1
+ # WorkRelay 语音合成(千问 Qwen3-TTS)扩展能力
2
+
3
+ Agent 语音回复能力:Docker 部署 **Voicebox**(**Qwen3-TTS CustomVoice 1.7B** 引擎),
4
+ 9 种预设音色(含中文女声 Vivian),中英日韩多语种,断句/语速自然,适合视频配音。
5
+
6
+ > 2026-09-14 新增。与 `workrelay-speak`(Kokoro)、`workrelay-speak-luxtts`(LuxTTS)**互斥**:
7
+ > TTS 同时只能启用一个,启用本能力会自动停止其它两个 TTS 容器并接管 `tts.provider`。
8
+
9
+ ## 文件说明
10
+
11
+ | 文件 | 作用 |
12
+ |------|------|
13
+ | `workrelay-tts.sh` | TTS 桥接脚本(调 voicebox-qwen REST API,固定引擎 qwen_custom_voice) |
14
+ | `install.sh` | 一键安装(拉镜像 → 下载千问模型 → 起容器 → 建音色 profile → 互斥 → 配 OpenClaw) |
15
+ | `status.sh` | 状态查询(含**互斥判定**:读 `tts.provider` 判断是否当前启用) |
16
+ | `manifest.json` | 前置条件清单(含镜像仓库可达性 + GPU 检测) |
17
+
18
+ ## 关键参数
19
+
20
+ | 项 | 值 |
21
+ |---|---|
22
+ | 容器名 | `voicebox-qwen` |
23
+ | 端口 | `17602`(Kokoro 17493/17600、LuxTTS 17601) |
24
+ | 镜像 | `crpi-...aliyuncs.com/shiduo/workrelay-tts:20260813`(复用,已含 qwen_custom_voice 后端) |
25
+ | 模型 | `Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice`(~4.6GB,安装时经 hf-mirror.com 下载) |
26
+ | 音色 profile | Vivian(中文女声);其它:Serena/Uncle_Fu/Dylan/Eric/Ryan/Aiden/Ono_Anna/Sohee |
27
+ | OpenClaw provider | `tts-local-cli-qwen`(配置在顶层 `tts.providers.*`) |
28
+ | 合成超时 | 300000 ms(5 分钟,CPU 推理较慢) |
29
+
30
+ ## 状态返回(status.sh)
31
+
32
+ ```json
33
+ {
34
+ "status": "enabled | disabled | partial",
35
+ "description": "…",
36
+ "endpoint": "http://127.0.0.1:17602",
37
+ "checks": {
38
+ "docker": true, "container": true, "api": true,
39
+ "tts_script": true, "profile": true,
40
+ "openclaw_config": true, "provider_active": true
41
+ }
42
+ }
43
+ ```
44
+
45
+ - `provider_active=true` → 当前 TTS 就是本引擎(`tts.provider=tts-local-cli-qwen`)
46
+ - `provider_active=false` → 已安装但被其它 TTS 接管 → 状态显示"已安装但未启用"
47
+
48
+ ## 管理命令
49
+
50
+ ```bash
51
+ docker ps | grep voicebox-qwen # 查看容器
52
+ docker logs -f voicebox-qwen # 查看日志
53
+ curl -s http://127.0.0.1:17602/profiles # 查看音色 profile
54
+ docker stop voicebox-qwen # 停用(并把 tts.provider 切回其它引擎)
55
+ ```
56
+
57
+ ## GPU 说明
58
+
59
+ - 有 NVIDIA GPU(`nvidia-smi` 可用)→ 安装时自动 `--gpus all`,合成快
60
+ - 无独显 → CPU 合成,1.7B 模型约 10~60 秒/句(慢但可用)
61
+
62
+ *文档版本:2026-09-14*
@@ -0,0 +1,349 @@
1
+ #!/bin/bash
2
+ # WorkRelay 语音合成(千问 Qwen3-TTS)一键安装脚本
3
+ # 2026-09-14 新增能力:
4
+ # - 独立容器 voicebox-qwen(端口 17602),与 Kokoro(17493) / LuxTTS(17601) 互不干扰
5
+ # - 复用 workrelay-tts 镜像(已内置 qwen_custom_voice 后端),安装时下载 Qwen3-TTS 1.7B 模型(~4.6GB)
6
+ # - TTS 互斥:启用本能力会自动停止其它 TTS 容器,并把 tts.provider 切到本引擎
7
+ set -eo pipefail
8
+
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ NC='\033[0m'
13
+
14
+ VB_DIR="$HOME/.workrelay/voicebox-qwen"
15
+ VB_HOST="${WR_VOICEBOX_HOST:-127.0.0.1}"
16
+ # 独立端口 17602(Kokoro 17493/17600、LuxTTS 17601,避免冲突)
17
+ VB_PORT="${WR_VOICEBOX_QWEN_PORT:-17602}"
18
+ VB_CONTAINER_PORT=17493
19
+ # 2026-09-14 双源:国内=阿里云公共仓库;国际=ghcr.io
20
+ # 2026-09-14 改名为 dlinkclaw-tts(统一仓库名,tag 区分模型;千问复用同源镜像,模型安装时下载)
21
+ IMG_TAG="${WR_IMAGE_TAG:-qwen-20260914}"
22
+ # 国内源:暂未发布(留空自动跳过);发布后填入 registry.cn-hangzhou.aliyuncs.com/NAMESPACE/dlinkclaw-tts:${IMG_TAG}
23
+ IMG_CN="${WR_IMAGE_CN:-}"
24
+ IMG_INTL="${WR_IMAGE_INTL:-ghcr.io/rockychen2016/dlinkclaw-tts:${IMG_TAG}}"
25
+ VB_IMAGE=""
26
+ # 千问模型(CustomVoice 预设音色版,1.7B)
27
+ QWEN_MODEL="${WR_QWEN_MODEL:-Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice}"
28
+ # 2026-09-14:模型下载源按区域选择——国内=hf-mirror.com(直连 HF 通常不通);国际=huggingface.co
29
+ if [ -n "${WR_HF_ENDPOINT:-}" ]; then
30
+ HF_MIRROR="$WR_HF_ENDPOINT"
31
+ elif [ "${WR_IMAGE_REGION:-auto}" = "intl" ]; then
32
+ HF_MIRROR="https://huggingface.co"
33
+ else
34
+ HF_MIRROR="https://hf-mirror.com"
35
+ fi
36
+ INSTALL_DIR="$(dirname "$(readlink -f "$0")")"
37
+
38
+ # ── 2026-09-14 镜像双源(国内=阿里云公共仓库 / 国际=ghcr.io)──
39
+ # 按 WR_IMAGE_REGION(auto|cn|intl)优先尝试,失败自动回退另一源;WR_VOICEBOX_IMAGE 可显式覆盖。
40
+ IMAGE_CANDIDATES=()
41
+ if [ -n "${WR_VOICEBOX_IMAGE:-}" ]; then
42
+ IMAGE_CANDIDATES=("$WR_VOICEBOX_IMAGE")
43
+ else
44
+ # 2026-09-14:国内源暂未发布(IMG_CN 为空则自动跳过),默认国际源优先;国内源上架后自动生效
45
+ case "${WR_IMAGE_REGION:-auto}" in
46
+ cn) CAND_PRIMARY="$IMG_CN"; CAND_SECONDARY="$IMG_INTL" ;;
47
+ intl) CAND_PRIMARY="$IMG_INTL"; CAND_SECONDARY="$IMG_CN" ;;
48
+ *) CAND_PRIMARY="$IMG_INTL"; CAND_SECONDARY="$IMG_CN" ;;
49
+ esac
50
+ [ -n "$CAND_PRIMARY" ] && IMAGE_CANDIDATES+=("$CAND_PRIMARY")
51
+ [ -n "$CAND_SECONDARY" ] && IMAGE_CANDIDATES+=("$CAND_SECONDARY")
52
+ fi
53
+
54
+ pull_image_with_fallback() {
55
+ local log="$1"
56
+ for img in "${IMAGE_CANDIDATES[@]}"; do
57
+ echo " → 尝试镜像源: $img"
58
+ if docker pull "$img" > "$log" 2>&1; then
59
+ VB_IMAGE="$img"
60
+ return 0
61
+ fi
62
+ echo " ⚠ 该源拉取失败,尝试下一个..."
63
+ done
64
+ return 1
65
+ }
66
+
67
+
68
+ echo "============================================"
69
+ echo " WorkRelay 语音合成(千问 Qwen3-TTS)- 一键安装"
70
+ echo "============================================"
71
+ echo ""
72
+
73
+ # ── 系统检查 ──
74
+ echo -n "[1/8] 检查系统环境... "
75
+
76
+ OS="$(uname -s)"
77
+ case "$OS" in
78
+ Linux) ;;
79
+ *)
80
+ echo -e "${RED}失败${NC}"
81
+ echo "错误:当前仅支持 Linux(TTS 需要 Docker)"
82
+ exit 1
83
+ ;;
84
+ esac
85
+
86
+ export PATH="$PATH:/snap/bin:/usr/local/bin"
87
+
88
+ if ! command -v docker &>/dev/null; then
89
+ echo -e "${RED}失败${NC}"
90
+ echo "错误:未找到 Docker,请先安装 Docker"
91
+ echo " Ubuntu: sudo apt install docker.io docker-compose-v2"
92
+ exit 1
93
+ fi
94
+
95
+ if ! docker info &>/dev/null 2>&1; then
96
+ # docker 权限多级自适应(尽量零 sudo)
97
+ if sg docker -c "docker info" &>/dev/null 2>&1; then
98
+ echo " ℹ 用户已加入 docker 组,通过 sg docker 执行(无需 sudo)"
99
+ docker() { sg docker -c "docker $(printf '%q ' "$@")"; }
100
+ elif sudo -n docker info &>/dev/null 2>&1; then
101
+ echo " ⚠ 当前用户无 docker 组权限,检测到免密 sudo,自动使用 sudo 执行 docker"
102
+ docker() { sudo docker "$@"; }
103
+ else
104
+ echo -e "${RED}失败${NC}"
105
+ echo "错误:Docker 未运行或无权限。请先执行一次以下命令:"
106
+ echo ""
107
+ echo " ① 启动 Docker:sudo systemctl start docker"
108
+ echo " ② 把当前用户加入 docker 组:sudo usermod -aG docker \$USER"
109
+ echo " ③ 重启 openclaw 服务(或重新登录会话),然后重新安装"
110
+ exit 1
111
+ fi
112
+ fi
113
+
114
+ echo -e "${GREEN}OK${NC} ($(docker --version | head -1), $(uname -o))"
115
+
116
+ # snap docker 项目目录适配(AppArmor 只放行 /var/snap/docker/common)
117
+ if [ -d /var/snap/docker/common ]; then
118
+ VB_DIR="/var/snap/docker/common/workrelay-voicebox-qwen"
119
+ echo " ℹ 检测到 snap docker,项目目录使用 $VB_DIR(AppArmor 放行读写)"
120
+ if [ -d "$VB_DIR" ]; then
121
+ sudo -n chown -R "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
122
+ elif ! sudo -n mkdir -p "$VB_DIR" 2>/dev/null; then
123
+ echo -e "${RED}失败${NC}"
124
+ echo "错误:无法创建项目目录 $VB_DIR(需要 root 权限)。"
125
+ echo "请手动执行一次以下命令后重新安装:"
126
+ echo " sudo mkdir -p $VB_DIR && sudo chown \$USER:\$USER $VB_DIR"
127
+ exit 1
128
+ fi
129
+ sudo -n chown "$(id -u):$(id -g)" "$VB_DIR" 2>/dev/null || true
130
+ fi
131
+
132
+ # ── 磁盘空间(模型 4.6GB + 镜像,建议 ≥8GB)──
133
+ echo -n "[2/8] 检查磁盘空间... "
134
+ DF_TARGET="$VB_DIR"; [ -d "$DF_TARGET" ] || DF_TARGET="$HOME"
135
+ AVAIL=$(df -BM "$DF_TARGET" | tail -1 | awk '{print $4}' | sed 's/M//')
136
+ if [ "$AVAIL" -lt 8192 ]; then
137
+ echo -e "${RED}不足${NC}"
138
+ echo "错误:可用空间 ${AVAIL}MB,至少需要 8GB(模型 4.6GB + 运行缓存)"
139
+ exit 1
140
+ fi
141
+ echo -e "${GREEN}OK${NC} (可用 ${AVAIL}MB)"
142
+
143
+ # ── 内存检查 ──
144
+ echo -n "[3/8] 检查内存... "
145
+ # 2026-09-15 修复:中文 locale 下 free -m 输出“内存:”,awk 匹配 Mem: 失败会误报内存不足 → 强制 LANG=C
146
+ RAM=$(LANG=C free -m | awk '/^Mem:/{print $2}')
147
+ if [ "$RAM" -lt 6000 ]; then
148
+ echo -e "${RED}不足 (${RAM}MB)${NC}"
149
+ echo "错误:至少需要 6GB 内存,当前仅 ${RAM}MB"
150
+ exit 1
151
+ fi
152
+ echo -e "${GREEN}OK${NC} (${RAM}MB)"
153
+
154
+ # ── GPU 检测(有 NVIDIA 则启用 CUDA 加速)──
155
+ echo -n "[4/8] 检测 GPU... "
156
+ DOCKER_GPU_ARGS=""
157
+ if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null 2>&1; then
158
+ echo -e "${GREEN}检测到 NVIDIA GPU${NC} ($(nvidia-smi -L 2>/dev/null | head -1))"
159
+ DOCKER_GPU_ARGS="--gpus all"
160
+ else
161
+ echo -e "${YELLOW}未检测到 N 卡,使用 CPU 合成(10~60 秒/句,较慢但可用)${NC}"
162
+ fi
163
+
164
+ # ── 端口检查 ──
165
+ echo -n "[5/8] 检查端口 $VB_PORT... "
166
+ if ss -tlnp 2>/dev/null | grep -q ":$VB_PORT "; then
167
+ echo -e "${RED}已被占用${NC}"
168
+ echo "错误:端口 $VB_PORT 已被占用,请先停止占用进程或设置 WR_VOICEBOX_QWEN_PORT 换端口"
169
+ exit 1
170
+ fi
171
+ echo -e "${GREEN}空闲${NC}"
172
+
173
+ # ── 拉取镜像 ──
174
+ echo -n "[6/8] 拉取 Voicebox 镜像... "
175
+ mkdir -p "$VB_DIR"
176
+ if pull_image_with_fallback /tmp/wr-pull-qwen.log; then
177
+ echo -e "${GREEN}OK${NC} ($VB_IMAGE)"
178
+ else
179
+ echo -e "${RED}失败${NC}"
180
+ tail -5 /tmp/wr-pull-qwen.log
181
+ echo "错误:所有镜像源均拉取失败,检查网络连接(国内=阿里云公共仓库 / 国际=ghcr.io)"
182
+ exit 1
183
+ fi
184
+
185
+ # ── 预下载千问模型(用镜像自带的 huggingface_hub + 国内镜像源)──
186
+ # 说明:镜像内已内置 Kokoro 模型,但未含 Qwen 模型;把 Qwen 模型下载到独立缓存目录,
187
+ # 容器启动时挂载该目录作为 HF 缓存(HF_HUB_OFFLINE=1 离线加载,避免容器内联网失败)。
188
+ echo -n "[7/8] 下载千问模型(约 4.6GB,视网速 5~30 分钟)... "
189
+ HF_CACHE="$VB_DIR/hf-cache"
190
+ mkdir -p "$HF_CACHE"
191
+ if docker run --rm \
192
+ -v "$HF_CACHE:/hf-cache" \
193
+ -e HF_ENDPOINT="$HF_MIRROR" \
194
+ -e HF_HUB_OFFLINE=0 \
195
+ -e HF_HUB_DISABLE_PROGRESS_BARS=1 \
196
+ --entrypoint python3 \
197
+ "$VB_IMAGE" -c "
198
+ from huggingface_hub import snapshot_download
199
+ p = snapshot_download('$QWEN_MODEL', cache_dir='/hf-cache')
200
+ print('MODEL_READY:' + p)
201
+ " > /tmp/wr-qwen-download.log 2>&1; then
202
+ echo -e "${GREEN}OK${NC}"
203
+ chmod -R a+rX "$HF_CACHE" 2>/dev/null || true
204
+ else
205
+ echo -e "${RED}失败${NC}"
206
+ tail -10 /tmp/wr-qwen-download.log
207
+ echo "错误:千问模型下载失败,请检查网络(hf-mirror.com)后重试"
208
+ exit 1
209
+ fi
210
+
211
+ # ── 启动容器 ──
212
+ echo "[8/8] 启动 Voicebox (千问) 容器..."
213
+
214
+ # 2026-09-15 修复(生产环境反馈):持久化整个 /app/data
215
+ # 含 voicebox.db(音色库)+ profiles + generations,避免容器重建后音色/数据丢失
216
+ mkdir -p "$VB_DIR/data/generations" "$VB_DIR/data/profiles" "$VB_DIR/data/cache"
217
+ chmod -R 777 "$VB_DIR/data" 2>/dev/null || true
218
+
219
+ docker rm -f voicebox-qwen >/dev/null 2>&1 || true
220
+ if ! docker run -d --name voicebox-qwen --restart unless-stopped \
221
+ -p "127.0.0.1:${VB_PORT}:${VB_CONTAINER_PORT}" \
222
+ -v "$VB_DIR/data:/app/data" \
223
+ -v "$HF_CACHE:/home/voicebox/.cache/huggingface" \
224
+ -e LOG_LEVEL=info \
225
+ -e NUMBA_CACHE_DIR=/tmp/numba_cache \
226
+ -e HF_HUB_OFFLINE=1 \
227
+ $DOCKER_GPU_ARGS \
228
+ --cpus 4 --memory 8g \
229
+ "$VB_IMAGE" >/dev/null 2>&1; then
230
+ echo -e "${RED}失败${NC}"
231
+ echo "错误:docker run 失败,执行 docker logs voicebox-qwen 查看日志"
232
+ exit 1
233
+ fi
234
+ echo " ✅ docker run 启动成功"
235
+
236
+ docker exec voicebox-qwen chmod -R 777 /app/data 2>/dev/null || true
237
+
238
+ # 等待 API 就绪
239
+ echo -n " 等待 Voicebox API 就绪..."
240
+ for i in $(seq 1 90); do
241
+ if curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1; then
242
+ echo -e " ${GREEN}OK${NC} (耗时 ${i} 次检查)"
243
+ break
244
+ fi
245
+ sleep 5
246
+ echo -n "."
247
+ done
248
+
249
+ if ! curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1; then
250
+ echo ""
251
+ echo -e "${RED}Voicebox 启动失败${NC}"
252
+ echo " 检查日志: docker logs voicebox-qwen"
253
+ exit 1
254
+ fi
255
+
256
+ # ── 创建千问预设音色 profile(Vivian 中文女声)──
257
+ echo " ⚙ 配置千问预设音色(Vivian 中文女声)..."
258
+
259
+ mkdir -p "$HOME/.workrelay/speak-qwen"
260
+ PROFILE_FILE="$HOME/.workrelay/speak-qwen/profile_id"
261
+
262
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 5 "http://${VB_HOST}:${VB_PORT}/profiles" 2>/dev/null \
263
+ | python3 -c '
264
+ import json,sys
265
+ try:
266
+ d=json.load(sys.stdin)
267
+ for p in d:
268
+ if p.get("preset_engine")=="qwen_custom_voice" and p.get("preset_voice_id")=="Vivian":
269
+ print(p.get("id","")); break
270
+ except Exception:
271
+ print("")
272
+ ' 2>/dev/null)
273
+
274
+ if [ -z "$EXIST_ID" ]; then
275
+ EXIST_ID=$(curl --noproxy '*' -s --max-time 15 -X POST "http://${VB_HOST}:${VB_PORT}/profiles" \
276
+ -H "Content-Type: application/json" \
277
+ -d '{"name":"WorkRelay 千问女声","description":"WorkRelay 千问 Qwen3-TTS 预设中文女声","language":"zh","voice_type":"preset","preset_engine":"qwen_custom_voice","preset_voice_id":"Vivian"}' \
278
+ | python3 -c 'import json,sys; print(json.load(sys.stdin).get("id",""))' 2>/dev/null)
279
+ fi
280
+
281
+ if [ -n "$EXIST_ID" ]; then
282
+ echo "$EXIST_ID" > "$PROFILE_FILE"
283
+ echo " ✅ 千问预设音色已就绪"
284
+ else
285
+ echo " ⚠ 预设音色创建失败,可手动设 WR_VOICEBOX_PROFILE_ID 指定声音"
286
+ fi
287
+
288
+ # ── TTS 互斥:停止其它 TTS 容器(TTS 只能启用一个)──
289
+ echo " 🔒 互斥处理:停止其它 TTS 容器..."
290
+ for other in voicebox voicebox-luxtts; do
291
+ if docker ps --format '{{.Names}}' 2>/dev/null | grep -qx "$other"; then
292
+ docker stop "$other" >/dev/null 2>&1 && echo " ⏹ 已停止 $other" || true
293
+ fi
294
+ done
295
+
296
+ # ── 部署脚本 & 配置 OpenClaw ──
297
+ echo -n "[收尾] 部署 TTS 桥接脚本 & 配置 OpenClaw... "
298
+
299
+ mkdir -p "$HOME/.workrelay/speak-qwen"
300
+ cp "$INSTALL_DIR/workrelay-tts.sh" "$HOME/.workrelay/speak-qwen/"
301
+ chmod +x "$HOME/.workrelay/speak-qwen/workrelay-tts.sh"
302
+ cp "$INSTALL_DIR/README.md" "$HOME/.workrelay/speak-qwen/" 2>/dev/null || true
303
+
304
+ if command -v openclaw &>/dev/null; then
305
+ TTS_SCRIPT="$HOME/.workrelay/speak-qwen/workrelay-tts.sh"
306
+ # 2026-09-14:openclaw 升级后 TTS 配置在顶层 tts.*(旧 messages.tts.* 已失效)
307
+ # 独立 provider 名 tts-local-cli-qwen,与 Kokoro(tts-local-cli)/LuxTTS(tts-local-cli-luxtts) 互不覆盖
308
+ openclaw config set tts.provider tts-local-cli-qwen 2>/dev/null || true
309
+ openclaw config set tts.providers.tts-local-cli-qwen.command "$TTS_SCRIPT" 2>/dev/null || true
310
+ openclaw config set tts.providers.tts-local-cli-qwen.args '["{{Text}}","{{OutputPath}}"]' 2>/dev/null || true
311
+ openclaw config set tts.providers.tts-local-cli-qwen.outputFormat wav 2>/dev/null || true
312
+ openclaw config set tts.providers.tts-local-cli-qwen.timeoutMs 300000 2>/dev/null || true
313
+
314
+ openclaw gateway restart 2>/dev/null || true
315
+ else
316
+ echo -e "${YELLOW}⚠ 未检测到 openclaw CLI,跳过自动配置${NC}"
317
+ fi
318
+
319
+ echo -e "${GREEN}OK${NC}"
320
+ echo ""
321
+
322
+ # ── 验证 ──
323
+ echo "============================================"
324
+ echo " 安装验证"
325
+ echo "============================================"
326
+
327
+ echo -n " Docker 容器: "
328
+ docker ps --format '{{.Names}}' 2>/dev/null | grep -q voicebox-qwen && echo -e "${GREEN}运行中${NC}" || echo -e "${RED}未运行${NC}"
329
+
330
+ echo -n " API 连通性: "
331
+ curl -s --max-time 2 "http://${VB_HOST}:${VB_PORT}/mcp" >/dev/null 2>&1 \
332
+ && echo -e "${GREEN}OK${NC} (http://${VB_HOST}:${VB_PORT})" \
333
+ || echo -e "${RED}无法连接${NC}"
334
+
335
+ echo -n " 千问 profile: "
336
+ [ -f "$PROFILE_FILE" ] && echo -e "${GREEN}$(cat "$PROFILE_FILE")${NC}" || echo -e "${RED}缺失${NC}"
337
+
338
+ echo -n " TTS provider: "
339
+ openclaw config get tts.provider 2>/dev/null | head -1 || echo "(读取失败)"
340
+
341
+ echo ""
342
+ echo -e "${GREEN}✅ 千问 Qwen3-TTS 安装完成!${NC}"
343
+ echo " API 地址: http://${VB_HOST}:${VB_PORT}"
344
+ echo " 管理命令: docker ps | grep voicebox-qwen"
345
+ echo " 查看日志: docker logs -f voicebox-qwen"
346
+ echo ""
347
+ echo " 🎙️ 音色:Vivian(中文女声);其它预设:Serena/Eric/Dylan/Ryan/Aiden 等 9 种"
348
+ echo " ⏱️ 首次合成需加载 1.7B 模型到内存,会稍慢(CPU 10~60 秒/句)"
349
+ echo ""