ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

microduck 远程控制台:用 Hugging Face Space 与 OAuth 驱动的离网机器人 WebRTC 控制页

2026/9/25 5:15:13 拓冰建站 浏览量
microduck 远程控制台:用 Hugging Face Space 与 OAuth 驱动的离网机器人 WebRTC 控制页 机器人嵌入式强化学习人工智能智能硬件计算机视觉音视频【免费下载链接】microduckA Tiny biped duck robot 项目地址https://gitcode.com/gh_mirrors/mi/microduck点击查看免费下载导读本文围绕 microduck 仓库中mediad/webclient/space/README.md这份 Space 元数据文档展开剖析这个双足小鸭子机器人microduck的远程控制台是如何被托管到 Hugging Face Space 上的机器人本体在局域网内自行提供同一个控制页而这份 Space 副本则让用户在机器人不在自己网络时也能登录并驾驶它。读完本文你将掌握hf_oauth: true这一行元数据为什么是整条远程访问链路的承重墙为什么一个不需要服务器的静态页面最终选择用 Docker Space 来托管entrypoint.sh如何把 OAuth 客户端凭据注入页面页面如何用同一套信令协议走通局域网 WebSocket与远程 SSE POST两种传输以及scripts/publish-console.sh是如何把这页从仓库发布到 Space 的。一、这份 README 是什么一个 Docker Space 的元数据卡片mediad/webclient/space/README.md不是一篇给人读的长文而是 Hugging Face Space 的配置入口。它顶部的 YAML front-matter 直接决定了这个 Space 的构建方式、端口、鉴权能力与展示信息--- title: microduck console emoji: colorFrom: yellow colorTo: gray sdk: docker app_port: 7860 pinned: false hf_oauth: true hf_oauth_expiration_minutes: 1440 short_description: Drive a duck that is not on your network. tags: - microduck - webrtc ---关键字段逐个拆解sdk: dockerSpace 以 Docker 方式运行而不是静态站点。正如下文要讲的这个选择是伤疤而非偏好——静态 Space 的变量注入在这个项目上失效过Docker 是验证过的、不会静默失效的替代路径。app_port: 7860容器内服务监听的端口与 mediad/webclient/space/Dockerfile 里的EXPOSE 7860严格一致Dockerfile 注释明说HFs default for a Docker Space, andapp_portin the README says the same number。hf_oauth: true这是整个卡片里最重要的一行。它让 Hugging Face 为这个 Space自动创建 OAuth 应用并把OAUTH_CLIENT_ID、OAUTH_SCOPES、OPENID_PROVIDER_URL等值放入容器的运行环境。README 原文用load-bearing承重这个词强调没有它控制台无法让任何人登录页面会直接明说这一点。hf_oauth_expiration_minutes: 1440登录态的有效时长1440 分钟即一天。页面把登录结果存在localStorage并在使用前检查过期时间。front-matter 之下的正文则回答了一个关键问题为什么远程控制台要长成现在这样。它点出了几个核心设计事实远程页与机器人本机伺服的是同一个页面mediad/webclient/index.html只是传输方式不同——局域网内走ws://robot:8443远程则经由 rendezvous 服务页面通过 Hugging Face 登录用 OAuth token 向 rendezvous 证明身份这份 Space 源码不应直接编辑它是从仓库发布的部署产物。二、为什么一个不需要服务器的页面用上了 Docker Space这是本 README 最有信息量的部分静态 Space 本应自动把window.huggingface.variables注入页面但对这个 Space 它从未生效过。据文档记录注入失败经历了多次尝试一次元数据变更、一次隐私开关切换、一次删除重建、甚至给页面补上真正的head让注入器有处可写此前页面长期是裸 doctype 内容。Hugging Face 自己的 API 报告两个 Space 完全一致——都是sdk: static、公开、hf_oauth: true、RUNNING——但一个能注入、一个不能。既然官方文档描述的注入路径不可依赖项目转向了 Docker Space 的另一条文档化路径hf_oauth: true把客户端 ID 放进环境变量容器自己把它写进页面。于是python3 -m http.server成了整个服务器。Dockerfile 里写得很直白# python -m http.server is the whole server: one file, GET only, no upload path, no config. The # alternative was nginx, which is a package, a config file and a user to get wrong for a page. WORKDIR /app COPY index.html /app/index.html COPY entrypoint.sh /app/entrypoint.sh RUN chmod x /app/entrypoint.sh EXPOSE 7860 ENTRYPOINT [/app/entrypoint.sh]选择python -m http.server而非 nginx 的理由是一个文件、只读 GET、无上传路径、无配置——对于一个只需把页面端出去的任务nginx 的一个包、一个配置文件、一个用户都是出错点。设计上的另一个微妙点Docker 不是为 Docker 而 Docker而是为了能拿到并注入 OAuth 客户端 ID。这也是 mediad/webclient/space/entrypoint.sh 的全部意义。三、entrypoint.sh把运行时变量写进页面的那八行 shentrypoint.sh是让远程登录能工作的机制核心。它做的事情可以概括为复刻静态 Space 本应注入的window.huggingface.variables对象但数据来自容器自身环境。3.1 注入哪些变量脚本定义了一个白名单PUBLIC (OAUTH_CLIENT_ID, OAUTH_SCOPES, OPENID_PROVIDER_URL, SPACE_HOST, SPACE_ID)只有这些静态 Space 会暴露给页面的变量会被写入缺失的变量直接省略而非输出null这样页面里的||兜底逻辑行为与静态 Space 完全一致。同时环境里确实存在OAUTH_CLIENT_SECRET但脚本故意不把它列入清单——浏览器端走的是 PKCE 流程不需要 secret客户端 ID 本身是公开值授权不了任何事。README 也强调secret 绝不能进入页面。3.2 用 Python 而非 sed 做替换注入不是简单字符串替换因为被注入的值现在是JSONprovider URL 里满是sed替换语法视为特殊字符的符号用sed会静默产出能解析但谁也登录不了的页面。因此脚本内嵌一段 Pythonvariables {name: os.environ[name] for name in PUBLIC if os.environ.get(name)} bootstrap scriptwindow.huggingface%s;/script % json.dumps({variables: variables})3.3 锚定head一次失败留下的教训注入位置也有讲究。页面 mediad/webclient/index.html 的注释反复强调html/head/body骨架是承重的——此前页面没有head导致没有任何地方可注入、没有客户端 ID、控制台无法登录且无任何报错。脚本用锚定正则定位独占一行的head开始标签HEAD re.compile(r^([ \t]*)head[ \t]*$, re.MULTILINE)要求恰好匹配一次HEAD.search未命中则退出提示页面无法让任何人登录找到两处head也拒绝猜测more than one line; refusing to guess之所以要锚定到行首是因为页面自身的注释里也含head字样——若做第一个匹配替换bootstrap 会被写进 HTML 注释里永不执行。3.4 注入失败时的行为脚本对环境里没有OAUTH_CLIENT_ID并不致命退出而是打印告警后照常起服务。README 解释了这一取舍a console that serves and explains beats one that will not start——一个能服务并解释原因的页面好过一个根本起不来的页面。最常见的诱因是 README 里少了hf_oauth: true。启动成功后exec python3 -m http.server 7860 --directory /srv对外提供页面。这套容器从环境变量注入 OAuth 变量的机制与telepresence的server.mjs以及spaces/policy-playground所用的机制一致spaces/policy-playground/entrypoint.sh、spaces/policy-playground/web/src/auth.ts里同样读取window.huggingface.variables。四、登录与作用域只要openid profiletoken 只用来证明身份4.1 为什么只要最小作用域页面登录只请求openid profile。README 明确指出这个 token 的全部工作是向 rendezvous 服务证明一个身份——服务通过whoami-v2解析 token 并从中读取用户名。向浏览器索要仓库级作用域如write-repos正是 docs/design/remote-access-design.md §2.4 要在机器人端修掉的那个错误该节记录了机器人设备流 token 携带全部作用域的问题并主张收敛为openid profile read-repos。页面代码 mediad/webclient/index.html 中的实现function oauthScopes() { return hfVariables().OAUTH_SCOPES || openid profile; }作用域从 Space 注入的对象读取而非写死在页面里因为 Space 的应用是按 README 元数据供给的OAUTH_SCOPES是 Hugging Face 回报它给这个应用配了什么一个请求自己应用并不拥有作用域的页面会产生谁都读不懂的拒绝。4.2 登录记住多久hf_oauth_expiration_minutesREADME 强调A sign-in lastshf_oauth_expiration_minutesand no longer.本 Space 为 1440 分钟即一天。页面把 OAuth 结果存入localStorage键为duck-console-oauth-v2并在把 token 交给 rendezvous之前检查过期时间过期的 token 视同未登录页面会重新发起登录而不是误报你的鸭子不在。页面代码rememberedSignIn()是这段逻辑的落点解析存储的 JSON、校验accessTokenExpiresAt过期即删除并重新登录。代码注释还记录了一个真实 bug——过期时间被记住但从未被读取导致一天后 header 仍显示你是谁、而 rendezvous 却解析不了 token表现为鸭子不在。登录链路的健壮性设计signIn()与resumeSignIn()同样值得注意每个 await 都与 10 秒超时赛跑SIGN_IN_TIMEOUT。原因地址栏残留一个已用过的?code会让oauthHandleRedirectIfPresent永久挂起页面卡在asking Hugging Face who you are…。超时消息会点名把 URL 里的 code 删掉。OAuth 重定向回调在页面加载时立即处理resumeSignIn挂在load事件上而不是等用户点 connect——否则 code 永远不被兑换形成登录循环。重定向 URI 必须精确匹配OAUTH_REDIRECT location.origin location.pathname含尾斜杠。签名库锁定版本huggingface/hub2.11.2HUB_ESM因为1曾解析到某个oauthHandleRedirectIfPresent不返回结果的版本把登录变成重定向循环。五、一页两传输同一套信令信封两条到达路径README 用一句话概括页面架构One page, two transports.一页、两种传输。这是理解整个 Space 的关键由机器人伺服时局域网场景页面从http://robot:8080/到达或用duckctl open自动定位打开ws://robot:8443直连机器人自己的信令服务器。因为页面就是目标机器人发的host 直接取location.hostname端口由 mediad 伺服时注入{{SIGNALLING_PORT}}无需输入任何地址。从此 Space 伺服时远程场景页面运行在 https 下浏览器根本不允许打开ws://混合内容被直接拦截因此它改为读取 rendezvous 服务的 SSE 事件流、以POST /send回发携带相同的信封只是带上了逐跳的 peer/session id。页面代码中的选择逻辑const SERVED_PORT Number({{SIGNALLING_PORT}}); const SERVED_BY_ROBOT Number.isFinite(SERVED_PORT); const REMOTE PARAMS.get(mode) ? PARAMS.get(mode) remote : !SERVED_BY_ROBOT location.protocol https:;?modelan/?moderemote参数可强制覆盖用于在本会选另一种传输的环境里测试某一种。两种传输共享同一套信令处理逻辑onSignalling()README 指出这正是 docs/design/remote-access-design.md §3.2 所说的对不透明 payload 做翻译的桥而非解析器。远程侧的send()有一个 LAN 场景不存在的陷阱startSession和list的应答会出现在POST /send的 HTTP 响应体里其余消息走事件流——代码注释记录了这个曾导致空 peer connection 失败的坑因此响应体会走与事件流相同的处理函数。5.1 远程事件流用 fetch 而非 EventSource因为 SSE 流需要Authorization头而EventSource无法设置请求头页面用fetch读取流并自行切分拒绝把 bearer token 放进 query string——那会出现在 Space 的访问日志和每一层代理里。实现要点包括入站时统一\r\n为\nSSE 允许 CRLF代理有权改写按\n\n切分会静默吞掉所有消息、只保留data:载荷、跳过注释型 ping。5.2 远程列表过滤meta.kind microduck远程模式下rendezvous 会列出该账号拥有的所有机器人包括reachy_mini家族的 producer。onSignalling对list应答按meta.kind过滤出 microduck避免把 mini 当作鸭子驾驶会给它发它不提供的方法名if (REMOTE) producers producers.filter((p) (p.meta || {}).kind KIND); // microduck账号名下有多只鸭子时出现选择器select idrobots单只则直接使用。producer 的meta由 mediad/src/producer.rs 填充name、serial、release、api_version在会话建立前就能让 header 显示机器人身份。5.3 远程页面还自带 TURN 中继凭据远程会话里页面还会以自己的 token向 TURN 凭据服务TURN_CREDENTIALS_URL即https://fastrtc-turn-service.hf.space/credentials换取 Cloudflare 中继凭据TTL 600 秒让自己这端也能提供 relay 候选。README 提及的动机记录在 docs/design/remote-access-design.md §6一条连接只需要一个可用的 relay 候选而这个候选必须来自浏览器这端——iPhone 在移动网络下没有 IPv4 套接字机器人提供的裸 IPv4 relay 字面量它根本发不出去。六、部署管线publish-console.sh与不要直接编辑 SpaceREADME 明确警告不要直接编辑这个 Space。页面源码mediad/webclient/index.html必须活在仓库里因为它要追踪两件同样活在仓库里的东西信令协议和机器人自己的方法名。一份放在 Space 仓库里的副本会与两者双双漂移。发布由 scripts/publish-console.sh 完成其要点用法scripts/publish-console.sh [--space org/name] [--dry-run]默认目标pollen-robotics/microduck-console推送需要具备该 Space 写权限的 HF tokenhf auth login存储脚本自身从不读取。替换两个 token{{API_VERSION}}从 duck-ipc-proto/src/lib.rs 的pub const API_VERSION读出并替换让页面能向使用者报告页面与机器人 API 版本不一致{{CONSOLE_BUILD}}替换为短 commit 页面自身 hash 时间戳页面加载时作为第一行日志打印——因为静态宿主缓存、浏览器缓存更狠一个小时的调试可能花在一个从未被加载的修复上页面必须能自己说出版本。{{SIGNALLING_PORT}}故意保留不替换。页面把未替换的端口解读为没有机器人伺服我——这在 Space 上恰好为真也正是它选择 rendezvous 传输的依据若替换掉页面会试图对 Space 打开 WebSocket。三道发布前自检页面必须有head否则 Space 无法注入 OAuth 客户端 ID页面不得出现OAUTH_CLIENT_SECRETPKCE 流程不需要 secret{{SIGNALLING_PORT}}必须仍在否则 Space 副本会走 WebSocket 路径。发布动作clone Space 仓库、覆盖index.html、README.md、Dockerfile、entrypoint.sh四个文件、git diff --quiet判断是否有变化、提交并推送提交信息如Console from microduck revision (api vN)。也就是说Space 目录README.mdDockerfileentrypoint.sh是部署清单页面本体才是真正的源代码两者由publish-console.sh捆绑发布。七、从页面源码看远程控制台能做什么虽然 Space README 是元数据卡片但它服务的页面mediad/webclient/index.html单文件、无构建步骤、无 npm本身就是远程驾驶一只鸭子的完整 UI。页面注释说得很清楚它直接说 gst-plugins-rs 的信令协议而非使用 gstwebrtc-api JS 库一个需要 npm 的客户端是没人会运行的客户端。页面能力包括视频与检测pc.ontrack把机器人推来的视频接到video检测框以 SVG 叠加坐标使用竖立帧自身像素相机侧装 90°画面旋转在 GPU 完成不耗 CPU——页面注释记录过 pipeline 里旋转曾花掉 145% 单核、CPU 97°C 并降频到 408 MHz 的教训。检测框 2.5 秒无更新即过期清除避免最后一只鸭子永远画在画面上。驾驶W/S/A/D、Q/E 转向全偏转 0.3 m/s 与 1.5 rad/s与padd默认一致意图以 10 Hz 持续重发机器人侧 500 ms 收不到即自行停止deadman。虚拟摇杆与键盘并存摇杆被按住时优先。姿态与技能robot.enable/robot.init/robot.relax/robot.stop/robot.shutdown技能列表从robot.policies读取技能是配置项写死清单会既多又少声音含chirp、greet、wheee (hold)等wheee是按住持续、松手释放的骑乘。遥测robot.subscribe2 Hz 帧 每 2 秒轮询robot.healthmode、policy、safety 标志、电池电压、电机/板卡温度limited_by直接说明限制原因。视线控制在画面上拖动即可让机器人看向某点robot.look解算 IK超出可达范围时页面显示 gaze clamped。原始 JSON-RPC 框抽屉里的 raw 输入框可发送任意方法包括两个被拒的示例按钮net.connect与system.pairingPinBLE 允许、WebRTC 拒绝用于证明路由表确实在被查询——这正是 mediad/src/route.rs 许可范围的体现。单文件约束页面依赖include_str!被 mediad 嵌入伺服到达它是一个地址而非一条命令http://robot:8080/。这些能力在两个传输下完全相同——LAN 与远程共用onSignalling、共用控制通道上的 JSON-RPC 匹配call/tell按 id 匹配应答这正是一页两传输设计成立的原因。八、安全与边界secret 不进页面、作用域最小化纵观整个 Space 设计安全边界是刻意且成体系的层措施凭据OAUTH_CLIENT_SECRET存在于容器环境但从不写入页面PKCE 浏览器流程不需要 secret且publish-console.sh对产物页面做 grep 自检作用域只请求openid profiletoken 的唯一职责是向 rendezvous 证明身份与 docs/design/remote-access-design.md §2.4 对机器人端 token 的收敛主张一致登录态过期即视为未登录hf_oauth_expiration_minutes宁可重新登录也不误报鸭子不在传输远程页在 https 下只能走 rendezvousws://被浏览器按混合内容拦截token 只进Authorization头、不进 query string注入注入正则锚定到独占一行的head且要求唯一bootstrap 不可能被写进注释页面还能通过?client_id覆盖客户端 ID——这是新应用在注册进任何地方之前先试用的通道与 docs/design/remote-access-design.md §5.0 记录的 GitHub Pages 备选方案同理页面从 URL 取客户端 ID意味着托管方可以随时更换。九、限制与前提需要说明的边界条件全部以当前仓库为准远程可达依赖外部服务rendezvouspollen-robotics-reachy-mini-central.hf.space与 TURN 凭据服务都是 Hugging Face 生态内的 Space页面在?rendezvous、?turn参数下可指向本地副本或假服务做测试但默认配置下远程控制台依赖这两者在线。登录是浏览器 OAuth 流与机器人端的设备码流robotctl account login/duckctl account login不同本页面用的是huggingface/hub的oauthLoginUrl/oauthHandleRedirectIfPresentPKCE、无 secret且登录库锁定2.11.2版本。账号名下才能列机器人页面只列出该 HF 账号拥有的 producer一只机器人需要先完成账号登录约在robotctl account login后三十秒内出现在列表且保持联网。不建议直接编辑 Space部署目标由scripts/publish-console.sh覆盖式发布页面源码的唯一真源在仓库内。十、结语这份 Space README 虽短却浓缩了 microduck 远程访问设计的全部关键决策hf_oauth: true提供 OAuth 应用与其环境变量、Docker Space 提供可靠的注入路径、entrypoint.sh提供运行时注入、index.html提供一页两传输的客户端、publish-console.sh提供从仓库到 Space 的发布管线。它回答了远程控制台最本质的那个问题——一个页面从哪里拿到 token答案是 Space 元数据本身。如果你要在自己的部署里复刻这套方案记住 README 里那句忠告即可别直接编辑 Space改仓库里的页面然后跑publish-console.sh而当登录失效时先检查 README 里hf_oauth: true是否还在——它是一切的地基。赞分享机器人嵌入式强化学习人工智能智能硬件计算机视觉音视频【免费下载链接】microduckA Tiny biped duck robot 项目地址https://gitcode.com/gh_mirrors/mi/microduck点击查看免费下载相关推荐microduck 的 WebRTC 控制台从测试页到机器人驾驶台mediad 与 duckctl 全链路实战microduck 的 WebRTC 控制台从测试页到机器人驾驶台mediad 与 duckctl 全链路实战 本指南围绕 microduck 仓库中 d机器人嵌入式强化学习人工智能智能硬件计算机视觉音视频microduck WebRTC 远程访问设计本地信令、双数据通道与控制通道如何接上机器人 APImicroduck WebRTC 远程访问设计本地信令、双数据通道与控制通道如何接上机器人 API 本篇基于 microduck 仓库的设计文档 remote机器人嵌入式强化学习人工智能智能硬件计算机视觉音视频AI驱动的数据分析Awesome Claude Skills业务智能平台AI驱动的数据分析Awesome Claude Skills业务智能平台 在当今数据驱动的商业环境中高效的数据分析能力已成为企业竞争的关键。AwesomeAI 技能AI 插件人工智能工作流自动化上一篇如何让微信对话成为永不消失的数字记忆WeChatMsg聊天记录备份完全指南下一篇如何用Accord.NET在10分钟内构建你的第一个分类器创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考