尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

小米AI音箱接入豆包(火山方舟)并实现联网搜索——群晖 NAS 部署完整实战教程

小米AI音箱接入豆包(火山方舟)并实现联网搜索——群晖 NAS 部署完整实战教程 小米AI音箱接入豆包火山方舟并实现联网搜索——群晖 NAS 部署完整实战教程重要声明隐私与安全本文是面向拥有「群晖 NAS 小爱音箱」的用户、可照抄执行的部署教程。文中出现的所有账号、API Key、IP 地址、密码、SSH 指纹等均为占位符形如YOUR_xxx需替换为你自己的。本文所有命令都在「集中配置文件 自动替换脚本」的机制下运行你把隐私填进本地的config.env 脚本会自动帮你生成运行时配置。免责声明本文为作者真实部署过程总结命令均经核实。因网络环境、版本差异个别细节请以官方文档为准。目录项目背景与需求总体技术方案与架构环境与前置准备关键技术点集中配置与自动替换核心机制完整操作步骤联网能力验证流式 SSE 事件格式补丁解析依据踩坑记录FAQ完整文件清单结语一、项目背景与需求家里有一台小米AI音箱初代MDZ-25-DA自带的小爱同学虽然能用但用户更想要一个更聪明且能够进行联网实时咨询的 AI 语音助手。市面上把第三方大模型接到小爱音箱的方案里最省事、最成熟的是mi-gpt。需求明确如下对小爱同学说“找豆包”进入和豆包的持续对话模式对话由豆包火山方舟/火山引擎大模型回答最关键豆包必须能联网检索例如查北京今天天气“今天有什么新闻”部署在群晖 NAS上用 Docker 跑7×24 稳定运行中文回复语音播报效果好适合家用。如果你也有一台群晖 NAS 一台小爱音箱跟着本文即可复现。二、总体技术方案与架构2.1 选型对比方案核心是小米音箱的语音 → mi-gpt唤醒、ASR、TTS、控制流 → LLM豆包 → 音箱 TTS 播报。LLM 这块我们最终选了火山方舟火山引擎方案联网能力成本说明OpenAI 官方需额外 Function Call需境外网络/绑卡国内不友好百度千帆需单独配置联网插件一般配置略复杂火山方舟豆包Responses API 原生支持 web_search低本文采用2.2 架构总览核心链路语音 → mi-gpt 监听小爱唤醒词 → 匹配callAIKeywords→ 调用 LLMLLM 调用走火山方舟 Responses API/api/v3/responses并开启web_search工具 → 豆包实时联网返回文本 → mi-gpt 交给小米 TTS 播报出来。三、环境与前置准备3.1 硬件与软件环境项目版本/参数智能音箱小米AI音箱初代MDZ-25-DANAS群晖 DS920DSMDocker 已启用SSH 已开启部署方式Docker 容器migpt-patched:keywords自建镜像大模型服务火山方舟火山引擎— 豆包 doubao-seed-evolving操作机Windows PowerShell通过pscp/plink操作 NAS3.2 需要申请的账号 / Key小米账号用来登录小米云mi-gpt 与音箱交互依赖建议使用与音箱绑定的那个小米账号。火山方舟 API Key在火山方舟控制台申请创建**接入点Endpoint**拿到形如ark-xxxx的 Key 和接入点 IDep-xxxx。群晖 NAS开启 SSH 服务确认能通过sudo执行命令Docker 位于/usr/local/bin/docker。四、关键技术点务必先看懂这几条是整个方案能跑通的核心也是踩坑最多的地方先讲清楚。4.1 火山方舟的两种接口与联网真相火山方舟提供两种 OpenAI 兼容接口联网能力差异巨大# ① Chat Completions 接口 —— 不支持联网的 web_search POST https://ark.cn-beijing.volces.com/api/v3/chat/completions Content‑Type: application/json { model: , messages: [...], tools: [{type:web_search}] } # ② Responses 接口 —— 支持联网检索 web_search ★ 用这个 POST https://ark.cn-beijing.volces.com/api/v3/responses Content‑Type: application/json { model: , input: [...], tools: [{type:web_search,max_keyword:2}] }重要结论亲测想联网必须走 Responses 接口并且请求参数用input不是messages联网工具写tools:[{type:web_search, max_keyword:N}]model可用接入点 IDep-xxx也可用模型名如deepseek-v4-flash-260425实测两种都能联网。若错用chat/completionsweb_search会报MissingParameter/Missing tools.function之类的错误——这是第 1 个大坑。4.2 mi-gpt 的 LLM 调用点mi-gpt 主程序是编译产物dist/index.cjs。其中openai.chat() → 记忆提取等子任务非主对话走 chat/completions openai.chatStream() → 主对话流式回答★ 我们改这里chatStream默认走this._client.chat.completions.createOpenAI SDK即 Chat Completions 接口 →无法联网。改造方案重写OpenAIClient.chatStream方法改用原生fetch直连火山方舟/responses接口并解析 SSE 流式事件同时保持方法签名不变参数{user, system, requestId, onStream, ...}、返回积累文本从而对 mi-gpt 上层零侵入。4.3 小米账号登录的两个坑异地登录短信验证码mi-gpt / mi-service-lite 首次登录小米云可能要短信验证码同一环境频繁切换会触发风控错误码70022 验证码发送过多请明天再试。令牌复用登录成功后 token 会存到.mi.json。用补丁让 mi-gpt直接复用已存令牌、跳过重新登录避免每次重启都要验证码。五、集中配置与自动替换核心机制为了让你不需要改任何代码里的 Key/IP/账号本项目采用「一个配置文件 一个自动替换脚本」的方式。你只需把真实信息填进本地的config.env脚本会生成运行时配置并把占位符替换成你的值。5.1 配置模板config.example.env# # mi-gpt 小爱音箱 豆包火山方舟部署配置模板 # 用法复制为 config.env替换 占位符运行 python3 setup_migpt.py # 注意config.env 含敏感信息请勿提交到公开仓库已加入 .gitignore # # ---- 1. 火山方舟豆包配置 ---- ARK_API_KEYYOUR_ARK_API_KEY ARK_API_MODELYOUR_ARK_ENDPOINT_OR_MODEL ARK_BASE_URLhttps://ark.cn-beijing.volces.com/api/v3 # ---- 2. 小米账号 ---- XIAOMI_USER_IDYOUR_XIAOMI_USER_ID XIAOMI_PASSWORDYOUR_XIAOMI_PASSWORD XIAOMI_SPEAKER_DIDYOUR_SPEAKER_NAME # ---- 3. NAS / Docker 远程操作配置 ---- NAS_HOSTYOUR_NAS_IP NAS_USERYOUR_NAS_USERNAME NAS_PASSWORDYOUR_NAS_PASSWORD NAS_HOST_KEYYOUR_SSH_HOSTKEY # ---- 4. Gitea可选---- GITEA_URLYOUR_GITEA_URL GITEA_USERYOUR_GITEA_USERNAME GITEA_PASSWORDYOUR_GITEA_PASSWORD5.2 自动替换脚本setup_migpt.py将config.env里的值自动注入并生成output/.env—— 供容器--env-file使用火山方舟 Key/模型output/.migpt.js—— mi-gpt 运行时配置小爱关键词/持续对话/设备名output/deploy.sh—— 可直接在 NAS 上照抄执行的部署命令含你的真实值。# 1) 复制配置模板Copy-Itemconfig.example.env config.env# 2) 编辑 config.env填入自己的真实信息用记事本或 VS Code# 3) 运行自动替换脚本python3 setup_migpt.py脚本会打印类似输出✔ 已生成以下文件 ...\output\.env ...\output\.migpt.js ...\output\deploy.sh 下一步 1. 打开 output/.env 与 output/.migpt.js 核对信息是否正确 2. 参考 output/deploy.sh 在 NAS 上执行或按本文手动上传执行。核心思路真实隐私只存在于你本地的config.env绝不进入博文、不进入公开仓库。这也是面向公开教程的安全正道。六、完整操作步骤下文所有命令中的YOUR_xxx均为占位符请替换为你在config.env里填写的真实值。演示操作机为 Windows PowerShell用pscp/plink连群晖若你直接用 NAS 的 SSH命令一致。6.1 准备目录与原始文件在群晖上创建目录sudomkdir-p/volume1/docker/tools/migpt-patchedsudomkdir-p/volume1/docker/migpt从官方镜像提取未打补丁的主程序作为补丁输入源sudo/usr/local/bin/docker create--namemigpt-tmp idootop/mi-gpt:latestsudo/usr/local/bin/dockercpmigpt-tmp:/app/dist/index.cjs /volume1/docker/tools/migpt-patched/dist_index_orig.cjssudo/usr/local/bin/dockerrmmigpt-tmp6.2 补丁 1关键词匹配 startsWith → includes小爱语音转写文本里关键词可能出现在句子中间原版startsWith只在最前面命中。改为includes后帮我打开豆包查天气这种也能触发。本补丁由patch_all.py一并完成。6.3 补丁 2chatStream 走火山方舟 Responses API联网核心将下面的patch_all.py放到群晖/volume1/docker/tools/migpt-patched/目录# -*- coding: utf-8 -*-# patch_all.py —— 群晖 /volume1/docker/tools/migpt-patched/patch_all.pyimportos SRC/volume1/docker/tools/migpt-patched/dist_index_orig.cjswithopen(SRC,r,encodingutf-8)asf:contentf.read()# ---- 补丁1: 关键词 startsWith - includes ----old1msg.text.startsWith(e)c1content.count(old1)contentcontent.replace(old1,msg.text.includes(e))print(f[1] 关键词匹配 startsWith-includes:{c1}处)# ---- 补丁2: chatStream 走火山方舟 Responses API(联网) ----chat_stream_new async chatStream(options) { var _a; this._init(); let { user, system, tools, jsonMode, requestId, onStream, trace false, model this.deployment ?? kEnvs.OPENAI_MODEL ?? gpt-4o } options; if (trace this.traceInput) { this._logger.log( \\u{1F525} onAskAI \\u{1F916}\\uFE0F System: ${system ?? None} \\u{1F60A} User: ${user}.trim() ); } const kEnvs2 kEnvs || process.env || {}; const base kEnvs2.OPENAI_BASE_URL || https://ark.cn-beijing.volces.com/api/v3; const apiKey kEnvs2.OPENAI_API_KEY; const input []; if (isNotEmpty(system)) { input.push({ role: system, content: system }); } input.push({ role: user, content: user }); let content ; let aborted false; if (requestId) { this._abortCallbacks[requestId] () { aborted true; }; } try { const resp await fetch(${base}/responses, { method: POST, headers: { Authorization: Bearer ${apiKey}, Content-Type: application/json }, body: JSON.stringify({ model, stream: true, tools: [{ type: web_search, max_keyword: 1 }], input }) }); if (!resp || !resp.body) { this._logger.error(LLM 响应异常(responses no body), resp null ? void 0 : resp.status); return void 0; } const reader resp.body.getReader(); const decoder new TextDecoder(); let buf ; for (;;) { if (requestId !Object.keys(this._abortCallbacks).includes(requestId)) { aborted true; } if (aborted) { try { await reader.cancel(); } catch (e2) {} break; } const { done, value } await reader.read(); if (done) break; buf decoder.decode(value, { stream: true }); let n; while ((n buf.indexOf(\\n\\n)) 0) { const rawEvent buf.slice(0, n); buf buf.slice(n 2); const dataLine (rawEvent.split(\\n).find((l) l.startsWith(data:))) || ; const data dataLine.slice(5).trim(); if (!data || data [DONE]) continue; let ev; try { ev JSON.parse(data); } catch (e3) { continue; } if (!ev || typeof ev ! object) continue; if (ev.type response.output_text.delta typeof ev.delta string) { if (onStream) onStream(ev.delta); content ev.delta; } else if (ev.type response.output_text.done typeof ev.text string ev.text !content) { if (onStream) onStream(ev.text); content ev.text; } else if (ev.type response.completed) { var arr ev.response ev.response.output ? ev.response.output : []; var full ; for (var oi 0; oi arr.length; oi) { var ob arr[oi]; if (ob.type message ob.content) { for (var ci 0; ci ob.content.length; ci) { var cc ob.content[ci]; if (cc.type output_text cc.text) full cc.text; } } } if (full !content) content full; } } } } catch (e) { this._logger.error(LLM 响应异常, e); return void 0; } if (requestId) { delete this._abortCallbacks[requestId]; } if (trace this.traceOutput) { this._logger.log(\\u2705 Answer: ${(content || ).trim() || None}.trim()); } return withDefault(content, void 0); } start_marker async chatStream(options) {end_marker return withDefault(content, void 0);\n }sicontent.find(start_marker)eicontent.find(end_marker)ifsi-1orei-1oreisi:print([2] !! 未找到 chatStream 方法锚点跳过保留原逻辑)else:ei_endeilen(end_marker)contentcontent[:si]chat_stream_newcontent[ei_end:]print(f[2] chatStream 已替换为 Responses API(联网) 版本)DST/volume1/docker/tools/migpt-patched/dist_index_patched2.cjswithopen(DST,w,encodingutf-8)asf:f.write(content)print(写出:,DST,os.path.getsize(DST),bytes)6.4 mi-service-lite 令牌复用补丁跳过短信验证码idootop/mi-gpt:latest内置的mi-service-lite会重新登录导致验证码。补丁改为存在有效 token 时复用.mi.json里的令牌、跳过重新登录日志会打印 使用已存的有效令牌连接小米云(跳过重新登录)。对应文件为index.cjsmi-service-lite 补丁放到群晖/volume1/docker/tools/migpt-patched/目录。6.5 构建 Dockerfile# /volume1/docker/tools/migpt-patched/Dockerfile FROM idootop/mi-gpt:latest # 主程序关键词匹配补丁startsWith - includes (让豆包在任何位置都能触发) COPY dist_index_patched2.cjs /app/dist/index.cjs # mi-service-lite 令牌复用补丁跳过异地登录 OTP COPY index.cjs /app/node_modules/.pnpm/mi-service-lite3.1.0/node_modules/mi-service-lite/dist/index.cjs6.6 上传文件到群晖并执行补丁这些操作也可直接用setup_migpt.py生成的output/deploy.sh一键完成。下面给出逐条命令。# 上传补丁脚本与 Dockerfilepscp-scp-pwYOUR_NAS_PASSWORD patch_all.py Dockerfile index.cjs cdrloverYOUR_NAS_HOST:/volume1/docker/tools/migpt-patched/# 在群晖执行综合补丁plink-ssh cdrloverYOUR_NAS_HOST-pwYOUR_NAS_PASSWORDcd /volume1/docker/tools/migpt-patched python3 patch_all.py预期输出[1] 关键词匹配 startsWith-includes: 5 处 [2] chatStream 已替换为 Responses API(联网) 版本 写出: /volume1/docker/tools/migpt-patched/dist_index_patched2.cjs 79596 bytes6.7 用容器校验语法不需要 sudoplink-ssh cdrloverYOUR_NAS_HOST-pwYOUR_NAS_PASSWORDecho YOUR_NAS_PASSWORD | sudo -S /usr/local/bin/docker run --rm -v /volume1/docker/tools/migpt-patched/dist_index_patched2.cjs:/chk.cjs --entrypoint node migpt-patched:latest --check /chk.cjs echo SYNTAX_OK看到SYNTAX_OK即语法通过。6.8 构建镜像、删除旧容器、启动新容器# 重建镜像plink-ssh cdrloverYOUR_NAS_HOST-pwYOUR_NAS_PASSWORDcd /volume1/docker/tools/migpt-patched echo YOUR_NAS_PASSWORD | sudo -S /usr/local/bin/docker build -t migpt-patched:keywords . 21 | tail -8# 删除旧容器plink-ssh cdrloverYOUR_NAS_HOST-pwYOUR_NAS_PASSWORDecho YOUR_NAS_PASSWORD | sudo -S /usr/local/bin/docker rm -f migpt# 启动新容器用生成的 .env / .migpt.jsplink-ssh cdrloverYOUR_NAS_HOST-pwYOUR_NAS_PASSWORDecho YOUR_NAS_PASSWORD | sudo -S /usr/local/bin/docker run -d --name migpt --env-file /volume1/docker/migpt/.env -v /volume1/docker/migpt/.migpt.js:/app/.migpt.js -v /volume1/docker/migpt/.mi.json:/app/.mi.json migpt-patched:keywords# 查看状态与日志plink-ssh cdrloverYOUR_NAS_HOST-pwYOUR_NAS_PASSWORDecho YOUR_NAS_PASSWORD | sudo -S /usr/local/bin/docker ps -a --format {{.Names}} | {{.Status}} | grep migptplink-ssh cdrloverYOUR_NAS_HOST-pwYOUR_NAS_PASSWORDecho YOUR_NAS_PASSWORD | sudo -S /usr/local/bin/docker logs --tail 20 migpt 21正常日志应包含 使用已存的有效令牌连接小米云(跳过重新登录) 使用已存的有效令牌连接小米云(跳过重新登录) Speaker ✅ 服务已启动...七、联网能力验证7.1 直接调火山方舟 Responses API 验证# test_responses.py —— 直接验证某模型/接入点是否能联网importurllib.request,json keyYOUR_ARK_API_KEYbaseYOUR_ARK_BASE_URL# 例如 https://ark.cn-beijing.volces.com/api/v3models[YOUR_ARK_ENDPOINT_OR_MODEL,ANOTHER_MODEL_IF_ANY]forminmodels:bodyjson.dumps({model:m,stream:False,tools:[{type:web_search,max_keyword:1}],input:[{role:user,content:北京今天天气}]}).encode()requrllib.request.Request(base/responses,body,headers{Authorization:Bearer key,Content-Type:application/json})try:rurllib.request.urlopen(req,timeout120)djson.loads(r.read())outd.get(output,[])text.join(cc.get(text,)forobinoutifob.get(type)messageforccinob.get(content,[])ifcc.get(type)output_text)wsany(ob.get(type)web_search_callforobinout)print(f{m} 联网调用:{ws}回答:{text[:80]})excepturllib.error.HTTPErrorase:print(f{m} HTTPError{e.code}:,e.read().decode()[:200])预期用你自己的 Key 替换后应看到联网调用: True且返回实时天气内容 —— 这就是联网成功的铁证。7.2 对音箱做端到端测试启动容器后对音箱说小爱同学找豆包 → 进入持续对话听到你好我是豆包… 小爱同学北京今天天气怎么样 → 应播报含实时气温/天气状况的联网结果 小爱同学关闭豆包 → 退出对话模式单次不进持续对话也可以小爱同学豆包今天天气怎么样7.3 看容器日志确认Open AI ✅ Answer: 北京今日天气2026年8月31日 星期一☀️ 天气状况白天晴间多云… → 音箱播报只要日志出现Open AI ✅ Answer:且内容包含实时数据而非我暂时无法联网即联网成功。八、流式 SSE 事件格式补丁解析依据火山方舟 Responses 接口stream:true返回的 SSE 事件类型实测抓包response.created response.in_progress response.output_item.added response.reasoning_summary_part.added # 思考过程(不用于输出) response.reasoning_summary_text.delta # 思考过程增量(忽略) response.reasoning_summary_text.done response.output_item.done response.content_part.added response.output_text.delta ★ 输出文本增量(取这个) response.output_text.done ★ 输出文本结束(含完整 text) response.content_part.done response.output_item.done response.completed ★ 全部完成(含完整 output)因此补丁只解析response.output_text.delta的delta字段进行累加即得完整回答reasoning_summary_text.delta是思考过程不要输出。九、踩坑记录FAQQ1用chat/completionsweb_search报错不会联网。必须用responses接口参数用inputtools:[{type:web_search,max_keyword:N}]。Q2did填错了导致音箱连不上did必须与音箱在米家 App 里的设备名称完全一致如小米AI音箱且大小写敏感。填错会报找不到设备。Q3反复要短信验证码被风控70022单日发送验证码过多会触发70022 验证码发送过多请明天再试解决成功登录后复用.mi.json令牌用 mi-service-lite 补丁跳过重新登录。Q4reasoning_summary_text.delta也被播报出来那是思考过程。补丁只取output_text.delta不会播报思考内容。Q5PowerShell 下 plink 嵌套引号报错PowerShell 嵌套引号极难转义。统一把脚本上传到 NAS 再执行如patch_all.py避免-c内联语法校验用容器node --check无需 sudo。Q6sudo: no password was provided群晖sudo需带密码写法echo 密码 | sudo -S 命令。Q7我的隐私信息Key/IP/账号怎么保护见第五节「集中配置与自动替换」。把真实信息只填在本地config.env已被.gitignore排除由setup_migpt.py自动生成运行时配置并做占位符替换。切勿把真实 Key/密码写进公开博文或推送到公开仓库。十、完整文件清单文件/位置用途是否含隐私config.example.env配置模板占位符复制为config.env否仅占位符config.env你填写的真实配置本地是勿上传/勿提交setup_migpt.py自动生成.env/.migpt.js/deploy.sh否output/.env运行时环境变量是只在本机/ NASoutput/.migpt.jsmi-gpt 运行时配置是只在本机/ NASoutput/deploy.shNAS 部署命令是含真实值勿上传patch_all.py双补丁脚本关键词 Responses API 联网否Dockerfile构建镜像否index.cjsmi-service-lite 令牌复用补丁否镜像migpt-patched:keywords最终运行镜像—容器migpt运行容器—# .gitignore —— 务必加入以下条目防止隐私泄露 config.env output/ .env .mi.json models.zip *.token十一、结语本文完整记录了从小米AI音箱 豆包到能联网查资讯的智能语音助手的全过程。核心突破点在于火山方舟必须走 Responses 接口并开启web_search才能联网重写 mi-gpt 的chatStream方法用原生fetch直连该接口并解析 SSE 流式事件在保持 mi-gpt 上层零侵入的前提下打通联网能力小爱关键词改为全局匹配includes小米令牌复用跳过验证码保证 7×24 稳定运行。再加上集中配置 自动替换的安全机制让读者填一个本地config.env即可完成私有化定制无需在公开内容里暴露任何隐私。部署完成后你的小爱音箱就能这样用小爱同学找豆包 小爱同学明天上海会下雨吗 小爱同学今天有什么科技新闻都能得到基于实时联网检索的、中文口语化的语音回答。附版本与更新组件版本 / 说明mi-gptv4.2.0idootop/mi-gpt:latestmi-service-lite3.1.0打了令牌复用补丁豆包模型doubao-seed-evolvingep-xxx接入点/ deepseek-v4-flash-260425接口火山方舟/api/v3/responsesweb_search
返回列表