每个 Sprint 都是 self-contained 的:把对应那节复制给一个新对话/新执行者,他不需要看本文档其他部分就能干完。 仅"项目上下文"那节是所有 Sprint 都要先读的公共部分。
- 本地:
/root/xiyu-ai-opensource - GitHub:
https://github.com/dimang01/xiyu-ai - 默认分支:
main - 当前版本:
v1.3.3(package.json)
- 绝不直推 main,auto 模式会拦。所有改动走分支 → PR → 合并:
git checkout main && git pull origin main git checkout -b <branch-name> # 改 → commit → push git push -u origin <branch-name> gh pr create --base main --head <branch-name> --title "…" --body "…" # CLEAN 后 gh pr merge <PR-num> --merge --delete-branch
- 发布版本:
git tag -a v1.4.x -m "…" && git push origin v1.4.x && gh release create … - 提交信息用项目风格:
feat:/hotfix:/chore:/docs: - 不要写出
Co-Authored-By: Claude之外的水印;首行 ≤ 72 字符
- Node.js ≥ 20,ESM only,
type: "module" - Express 5、better-sqlite3、dotenv
- 数据库:SQLite 单文件
data/bot.db,迁移用addColIfMissing/CREATE TABLE IF NOT EXISTS模式(见src/db.mjs的migrateXxx()函数列表) - 前端:纯静态 HTML + Tailwind CDN + 自家
public/app/glass.css液态玻璃层
| 文件 | 职责 |
|---|---|
src/providers/{chat,vision,asr,image,embedding}.mjs |
5 类能力的 provider 抽象 + REGISTRY |
src/ilink.mjs |
微信 iLink 长轮询 + sendmessage + media 上传 |
src/media.mjs |
CDN AES-128-ECB 加密上传(图片已用,voice 复用) |
src/bot.mjs |
入站消息主处理器 |
src/proactive.mjs |
主动消息调度(早安/晚安/告白/纪念日/场景照) |
src/companion.mjs |
buildSystemPrompt() 18 节人设拼接 |
src/emotion_state.mjs |
7 维情绪状态机 |
src/db.mjs |
全部 SQLite + migrate 注册点 |
src/api.mjs |
REST API,requireOwnedCompanion 鉴权范式 |
node --check <file>— 改动后必查bash scripts/opensource_check.sh— 6/6 通过node scripts/p0_regression_check.mjs— source-level 107 项;末尾 13 项 HTTP 失败是 :3000 老服务占端口,与改动无关,忽略- 临时启动冒烟:
DB_PATH=/tmp/x.db API_PORT=3990 PORT=3990 AUTH_MODE=local timeout 7 node index.mjs > /tmp/x.log 2>&1 & sleep 4 && curl -s -o /dev/null -w "%{http_code}\n" http://127.0.0.1:3990/api/health -m 3 kill %1; rm -f /tmp/x.db*
- v1.2.10 SW + 默认采样 (#18)
- v1.2.11 她的日记 (#19) — provider 抽象 + cron + DB 迁移 + API + 前端页 = 全套范式
- v1.2.12 纪念日主动祝福 (#20)
- v1.3.0 液态玻璃 UI (#21)
- v1.3.3 主动消息滑块 (#27) — dashboard 加 slider + DB 字段 + 前端联动 = 范式
让"她"能在微信主动给用户发语音消息(晚安/告白/纪念日等场景),覆盖入站 ASR + 出站 TTS 完整"听+说"闭环。
- iLink
sendmessagevoice 走msg_type=3、encode_type=6 (SILK) - 音频要先转 SILK v3(24kHz mono PCM 编码)
- CDN 上传 AES-128-ECB 加密,本仓
src/media.mjs::uploadFile已实现,voice 走同套 - TTS 输出通常是 mp3/wav,必须本地转码到 SILK → 用
wx-voicenpm 包 + ffmpeg
默认 MiniMax speech-2.8(本仓 chat 已支持 minimax,复用账号;新户送 500 字符); 情感场景进阶 火山豆包 TTS 2.0(指令式情感控制,配合 emotion_state 是亮点)。 Sprint 1 只先做 MiniMax;其他在 Sprint 3 补齐。
让后端能拿一段中文文本 → 调 MiniMax TTS → 返回 SILK 字节 + 时长(毫秒),并提供一个 /api/companions/:id/tts-preview 路由给前端试听 mp3。不接微信、不发任何东西。
npm install wx-voice --save- Dockerfile 在
apt-get install行加ffmpeg - 本地装 ffmpeg 验证:
ffmpeg -version - 验收:
node -e "require('wx-voice')"不报错
仿 src/providers/vision.mjs 范式(OpenAI-compat 优先):
- 导出
REGISTRY,先只有minimax一项minimax: { baseURL: 'https://api.minimax.chat/v1', defaultModel: 'speech-2.8', apiKeyEnv: 'MINIMAX_API_KEY', label: 'MiniMax speech-2.8', kind: 'minimax-native', // 不是 openai-compat }
- 导出
getActiveProviderName()/getApiKey()(同 vision.mjs 模式:env → app_settings → 报错) - 导出
async function ttsSynthesize(text, { voice_id, speed, emotion } = {})- 返回
{ audio: Buffer, format: 'mp3' }(先不转码)
- 返回
- MiniMax 调用文档查官方:
POST /v1/t2a_v2withvoice_id/speed/emotion - 失败:抛 Error 含
[tts]前缀,让 caller 决定降级
- 导出
async function synthesizeAndConvertToSilk(text, opts):- 调
ttsSynthesize拿 mp3 Buffer - 写临时文件(用
os.tmpdir()+ 随机名) - 用 wx-voice 编码到 SILK,得 silk 文件 + duration_ms
- 读回字节 → 删临时文件 → 返回
{ silk: Buffer, duration_ms: number, mp3: Buffer }
- 调
- 全程 try/finally 删临时文件,任何一步失败都不能留垃圾
- 失败抛 Error,不要 fallback 到文本(这是 Sprint 2 的事)
在 src/db.mjs 加 migrateVoiceReply(),注册到迁移列表(紧跟在 migrateProactiveDailyTarget 后面):
addColIfMissing('companions', 'voice_reply_enabled', 'INTEGER DEFAULT 0')addColIfMissing('companions', 'voice_id', 'TEXT')— provider 自家的 voice idaddColIfMissing('companions', 'voice_speed', 'REAL DEFAULT 1.0')(这个字段早就在 schema 里,跳过,addColIfMissing 自身幂等)- 把
voice_reply_enabled/voice_id加进ALLOWED_FIELDS
在 src/api.mjs 加(仿 /diary 范式):
POST /api/companions/:id/tts-preview
body: { text: string }
→ audio/mpeg (mp3 字节)
requireOwnedCompanion鉴权- text 长度上限 100 字符,防滥用
- 直接返回 mp3 字节流(不返 SILK,浏览器播放 mp3 简单),SILK 转码留给微信路径用
- res.setHeader('Content-Type', 'audio/mpeg')
写一个 scripts/voice_smoke.mjs(不进 release,用完删/或留作开发工具):
- 读 MINIMAX_API_KEY 环境变量
- 调
synthesizeAndConvertToSilk('你好我是溪语') - 写
/tmp/voice_test.silk和/tmp/voice_test.mp3 - console.log duration_ms 和文件大小
-
node --check src/providers/tts.mjs src/voice_pipeline.mjs src/db.mjs src/api.mjs全过 -
bash scripts/opensource_check.sh6/6 - 启动冒烟:
/api/health200 - 配上真 MINIMAX_API_KEY,
scripts/voice_smoke.mjs跑出 mp3 + silk 两个文件 - mp3 用本机播放器能听见
- silk 用 wx-voice 反解回 mp3 也能听见
- 分支:
v1.4.0-alpha-tts-pipeline - PR 标题:
feat: TTS pipeline (MiniMax + wx-voice) [v1.4.0-alpha] - 不打 tag、不发 release,这是 alpha
- package.json 不升版本
- ❌ 不要接 iLink 发语音
- ❌ 不要改 proactive.mjs / bot.mjs
- ❌ 不要加 setup wizard 或 dashboard UI
- ❌ 不要做豆包/Azure/OpenAI provider
- ❌ 不要做降级、用量上限、情感映射
Sprint 1 已合并进 main。
让"她"能在三个高价值场景真的发出微信语音:晚安 / 告白 / 纪念日。用户可在 dashboard 一键开关。默认关,失败优雅降级到文本。
仿 sendMessageItem 的实现:
- 参数:
ctx, toUserId, silkBuffer, duration_ms, contextToken - 走
src/media.mjs::uploadFile上传 silk 字节 → 拿 voice item - 调
sendMessage把 voice item 当msg_items[0]发出 - 包 setLastStatus / log(同 sendImage 风格)
- voice 的
encode_type=6、item.type='voice'、voice_item.duration_ms
参考:src/media.mjs::uploadFile 已经处理了 CDN AES,沿用即可。如果 uploadFile 对 voice 类型有缺漏,按需扩展(保持图片路径不破)。
只在 proactive.mjs 接,bot.mjs 暂时不接(普通对话自动转语音风险高,先稳):
在 src/proactive.mjs::sendProactiveMessage 加分支:
- 仅当
companion.voice_reply_enabled === 1 - 仅当
effectiveKind∈['goodnight', 'confession', 'reminder'] - 仅当 reply 长度 ≤ 60 字(太长的语音体验差 + 烧 token)
流程:
if (shouldVoice) {
try {
const { silk, duration_ms } = await synthesizeAndConvertToSilk(reply, {
voice_id: companion.voice_id || DEFAULT_VOICE_ID,
speed: companion.voice_speed || 1.0,
});
await sendVoiceMessage(ctx, companion.wechat_user_id, silk, duration_ms, null);
// 还要保存对话轮 + 用量 + log
return;
} catch (e) {
log('warn', `[Proactive] voice 失败 fallback 文本 companion=${companion.id}: ${e.message}`);
// 继续走原文本路径
}
}
// 原 sendTextMessage 路径关键:voice 失败必须无感降级到文本,不能因为 TTS 挂了就什么都不发。
新增 src/db.mjs::recordVoiceUsage(companionId, charCount) + getVoiceUsageToday(companionId):
- 新表
companion_voice_usage(companion_id, date_key, char_count, count)UNIQUE(companion_id, date_key) - proactive 发语音前查当日字符数,超过
VOICE_DAILY_CHAR_LIMIT(env 默认 2000)就跳过 voice 走文本 - 失败也不计数
在「主动找你」卡片下面(v1.3.3 加滑块那块),加:
- 一个 toggle「语音回复」(
t-voice),绑companion.voice_reply_enabled - 一个细灰字提示:"仅在晚安/告白/纪念日时发语音,最多 N 字符/天"
完全照 v1.3.3 改动 dashboard 的模式,不再赘述。
Sprint 1 路由复用,但 voice_id / speed 改成从 query 读:
- 让 dashboard 后续做"试听"按钮时能传当前选择的 voice_id 测试
- 单测:mock TTS provider 返回固定字节,验证 sendVoiceMessage 调用参数正确
- mock TTS 抛错,验证 proactive 优雅降级到文本
- 所有 syntax / opensource_check / 启动冒烟通过
- dashboard 开关能存能读
- 改 companion.voice_reply_enabled=1 + 真实 MINIMAX_API_KEY + 等到晚安触发,微信能收到语音
- 故意配错 API key,微信收到的是文本晚安(降级生效)
- 当日用量超限后,下次触发收到的是文本
- 普通对话回复仍然全是文本(bot.mjs 没改)
- 分支:
v1.4.0-beta-wechat-voice - PR 标题:
feat: WeChat outbound voice for goodnight/confession/reminder [v1.4.0-beta] - 不打 tag、不发 release,这是 beta
- 内部测试 3-5 天观察微信账号有没有受限
- ❌ 不要让 bot.mjs 的普通对话回复转语音(容易触发风控)
- ❌ 不要做声音克隆、emotion 映射
- ❌ 不要做 setup wizard 入口(v1.4.0-stable 阶段做)
Sprint 2 beta 已经在你的真实微信号上跑 3-5 天,没被风控。如果被风控,先调 trigger 阈值再继续。
TTS Provider 加齐(豆包、Azure、OpenAI),情绪状态机接通豆包指令式情感,setup wizard 加第四块「🔊 语音回复」,p0_regression 加 voice 用例。
src/providers/tts.mjsREGISTRY 加doubao项(火山方舟接入点 ID 模式,类似 vision 那家)- 注意豆包按年付 + 按音色,文档示例填
voice_type=zh_male_xxx - 豆包指令式情感接
voice_pipeline.mjs:- 加
opts.emotion('happy'|'sad'|'tender'|'whisper'…) - 调用时塞进 doubao 请求 body
- 其他 provider 忽略这参数
- 加
- Azure:
baseURL走https://<region>.tts.speech.microsoft.com,SSML 格式 - OpenAI:
/v1/audio/speech,model=tts-1 - 两家都加进 REGISTRY,setup wizard 自动出现选项
在 src/emotion_state.mjs 加:
export function emotionStateToVoiceEmotion(emotionState) {
// affection >= 70 + mood='happy' → 'tender'
// mood='sad' → 'sad'
// energy < 30 → 'whisper'
// 默认 'neutral'
}proactive.mjs 调用 voice_pipeline 时传这个 emotion,仅豆包/支持的 provider 会用。
在 public/app/setup.html 的 vision/asr 旁加「🔊 语音回复(tts)」折叠块:
- Provider 下拉:不启用 / MiniMax / 豆包 / Azure / OpenAI
- Model / API Key 输入框
- 保存 / 清除按钮
- 完全照 vision 那块复制
后端 /api/setup/provider-config 加 capability === 'tts' 分支(同 vision/asr)。
dashboard 「语音回复」toggle 旁加按钮:
- 点击 → 弹"输入想试听的文本" → 调
/api/companions/:id/tts-preview?text=… - 拿 mp3 直接
<audio>播放 - 用浏览器自带 Audio API,不依赖第三方播放器
scripts/p0_regression_check.mjs 加:
- 检查
src/providers/tts.mjs存在 - 检查
src/voice_pipeline.mjs存在 - 检查
companion_voice_usage表能查 - 检查
/api/companions/:id/tts-preview未登录返回 401
按情况调整 proactive 触发条件 / 加更长冷却 / 减少最大每日 voice 数。
- 4 家 provider 都能在 setup wizard 配 + 试听都能出声
- dashboard 试听按钮工作
- 豆包 voice 在情绪 happy/sad/whisper 时听感不同
- p0 通过,opensource_check 6/6
- 分支:
v1.4.0-rc-providers - PR 标题:
feat: 4-provider TTS + emotion-aware voice [v1.4.0-rc] - 还是不打 tag,等 Sprint 4 一起发
Sprint 3 已合并进 main,所有功能完整。
让外部用户能直接 docker pull ghcr.io/dimang01/xiyu-ai:1.4.0 上手。
在「核心特性」表加一行:
| 🔊 **语音回复** *(v1.4.0)* | 晚安/告白/纪念日她会发微信语音;4 家 TTS provider 可选(MiniMax/豆包/Azure/OpenAI);豆包情绪联动;每日字符上限;失败自动降级文本 |
加一段 v1.4.0 更新(顶部,类似 v1.3.0 那段)。
英文部分同步。
详细指南,目标受众:自托管用户。内容:
- 概念:什么是 TTS、什么是 SILK、为什么需要 ffmpeg
- 4 家 provider 注册流程、API key 在哪拿、定价
- ffmpeg 装哪里、Docker 用户用 1.4.0 镜像就自带
- dashboard 怎么开启
- 故障排查:发不出语音怎么办、试听能听见但微信收不到怎么办、被风控怎么办
- 合规提醒:AI 生成内容需显著标识
在 docs/ROADMAP.md 加:
## P2C · Voice — ✅ v1.4.0 Done
| Area | Feature | Status |
| Voice | TTS provider abstraction (4 providers) | ✅ |
| Voice | mp3 → SILK pipeline | ✅ |
| Voice | iLink sendVoiceMessage | ✅ |
| Voice | Goodnight/confession/reminder voice | ✅ |
| Voice | Emotion-aware voice (Doubao) | ✅ |
| Voice | Daily char limit + graceful fallback | ✅ |
| Voice | Setup wizard + dashboard toggle + preview | ✅ |
- Dockerfile 已经在 Sprint 1 加了 ffmpeg
- 确认双架构(amd64 + arm64)能 build
- GitHub Actions 在 tag v1.4.0 时自动构建 + 推 GHCR
- 本地 smoke:
docker run -p 3000:3000 -v xiyu-data:/app/data ghcr.io/dimang01/xiyu-ai:1.4.0
按 v1.3.x 提交流程走 PR。
git tag -a v1.4.0 -m "v1.4.0: Voice replies — She speaks now"
git push origin v1.4.0
gh release create v1.4.0 --title "v1.4.0 — She speaks now" --notes "…"
Release notes 模板:
## ✨ 新增:主动发语音
晚安 / 告白 / 纪念日她会发微信语音了。
- 4 家 TTS provider 任选:MiniMax(推荐入门)/ 豆包(情绪最强)/ Azure / OpenAI
- 豆包 provider 自动接情绪状态机:开心时温柔、累时轻声、低落时缓慢
- 默认关,dashboard 一键开
- 每日字符上限保护,失败自动降级文本
- 完整指南见 docs/voice-setup.md
## 升级
docker pull ghcr.io/dimang01/xiyu-ai:1.4.0
(自带 ffmpeg)
- README 渲染正常、英文段完整
- docs/voice-setup.md 第一次读的人能照着配出来
- Docker 镜像在 amd64 + arm64 都能拉、能跑、能发出 voice
- v1.4.0 release 出现在 GitHub releases 页且标 Latest
- ❌ 不要 v1.4.0 还塞新功能,文档/打包/发布而已
- ❌ 不要忘了清浏览器缓存验证页面新文案
feat:新功能hotfix:紧急修复(用户报告问题)fix:普通修复chore:版本/依赖/小杂活docs:纯文档
- 每个 Sprint 的"验收"清单全部要勾,不勾不能开 PR
- 启动冒烟是最低门槛,2 分钟的事
- 真机微信测试是 Sprint 2/3 的必经环节,没有真号就不要开 v1.4.0-stable PR
- 任何路径上 TTS 挂了,用户必须能收到文本
- 任何路径上转码挂了,用户必须能收到文本
- 任何路径上 sendVoiceMessage 挂了,用户必须能收到文本
- Sprint 2 beta 阶段必须 3-5 天连续观察自己微信账号
- 任何异常(被限制、警告)立刻停发 voice,调阈值
- ❌ 不要修改
src/ilink.mjs::sendMessage核心路径(图片/文本路径已稳定) - ❌ 不要给入站 bot.mjs 默认开自动语音回复(风控高危)
- ❌ 不要把 voice_id / API key 写入日志
- ❌ 不要在没有 try/finally 的情况下创建临时文件
| Sprint | 范围 | 工时 | 累计 |
|---|---|---|---|
| 1 | TTS 链路 | 5 天 | 5 天 |
| 2 | 接微信 + beta 观察 | 5 天 + 3-5 天观察 | 13 天 |
| 3 | 多 provider + 情绪 + setup wizard | 4-7 天 | 20 天 |
| 4 | 文档 + Docker + 发布 | 2-3 天 | 23 天 |
实际节奏:3-4 周日历时间(人不会每天 8h 全投)。
文档生成于 v1.3.3 之后。如果开干时本仓已有新版本,优先看 main HEAD 实际情况。
2026-05-30 实测得到结论:iLink/ClawBot 协议禁止 bot outbound voice。
- 实测:构造正确的 voice_item(playtime_ms / sample_rate=24000 / encode_type=6 SILK)+ 正确的 CDN media_type=VOICE(4) + 正确的 context_token → HTTP 200 但消息静默丢弃,微信端从未收到。
- 腾讯官方 SDK
@tencent-weixin/openclaw-weixin的src/messaging/send.js只实现 sendImage / sendVideo / sendFile,没有 sendVoiceMessageWeixin。注释明确:"image send uses sendImageMessageWeixin"——voice 路径完全缺位。 - README / CHANGELOG 里 voice 词频为 0,从未作为 outbound 功能描述。
- 推测原因:腾讯反欺诈策略,不允许 bot 伪装真人发语音。
- Sprint 2 的
maybeSendVoice/proactive.mjs微信端 voice 触发 已撤回 (v1.4.0 Sprint 2.5) src/ilink.mjs::sendVoiceMessage函数本身留着,万一腾讯将来放开协议即插即用- TTS pipeline / SILK 转码 /
tts-previewAPI 全部留着不变
已交付(PR #?):
- T1: 撤
proactive.mjs::maybeSendVoice及相关 import - T2:
POST /api/companions/:id/asr-transcribe—— Playground 录音 → ASR → 文字 → 走原 chat 流 - T3: Playground 每条 assistant 回复加 🔊 朗读按钮(浏览器 Audio 播 mp3)
- T4: Diary 页加全文朗读(按句号切段、依次播)
- T5: Dashboard 语音卡片改名「网页语音体验」,文案明示"iLink 协议不支持微信端"
- T6: 本文档更新
原 Sprint 3 计划:4 家 provider + 情绪映射 + setup wizard 接入。 调整为:保留多 provider + setup wizard 入口,把"情绪驱动"用在浏览器朗读速度/音调动态调整而不是发到微信。