dsh-feyagate-gateway
这个文件解决什么问题:让三类读者各自在 3 分钟内找到自己要看的东西 —— 想装的人拿到最短安装路径,想贡献的人拿到开发环境与目录职责,关心合规的人拿到"仓库里到底有什么、没有什么"。
npm 包名 dsh-feyagate · 版本 0.3.6
0.3.6:新增「开放接口」页 + 引擎改用产品名 + 反馈入口(界面与文案层,宿主能力未变)
- 新增第 7 个分区「开放接口」:把网关的 MCP 与 HTTP 两个接口面搬进设置页 ——
MCP 地址按实时端口拼出并可一键复制、「接入配置片段」下拉可在通用
mcpServers
JSON 与纯地址之间切换;HTTP 端点速查表每行带复制 curl(引号已按 CMD / POSIX
双兼容转义);另有 API 文档入口与服务未运行时的明确提示。
文档正文见 docs/api.zh.md(MCP 握手、工具调用示例、端点参数、
端口发现方式、安全边界与常见问题),页面上只放链接,保证只有一份正文要维护。
- 引擎统一叫「FeyaGate MCP 引擎」:
miloco-mcp-server 是实现细节,界面不再要求
用户认识 "miloco" 这个词(见 ENGINE_DISPLAY_NAME);下载文件名示例等事实性
文案仍然照实写。
- 顶栏第二行改为常驻入口:「GitHub 仓库」「问题反馈」直接跳转,不再显示
"免费试用还剩 N 天" —— 各平台剩余天数各不相同、0 又表示"没有试用期/已用完",
压缩成一句只会误导;每个平台的详情回到「授权 › 平台能力」看。
- 日志页新增「去 GitHub 反馈」,配合「复制全部」把日志带到 Issues。
- 「门面端口」更名为 「MCP server 端口」(字段与行为不变,只是叫法按用户视角)。
0.3.5(历史):设置页交互重做 + 授权页免费版文案修正(纯界面与展示层,宿主能力未变)
-
一屏只有一个主按钮。 顶部常驻状态条右侧只出现"现在该做的那一件事"(没装→安装、
没跑→启动、一切正常→不放按钮);原来"顶部大按钮 + 页内按钮"两个入口会各自触发安装,
现在两处共用同一段逻辑(同一个 busy、同一套错误文案)。
-
版本卡合并。 原「版本」+「指定版本」两张卡合成一张:一句结论(尚未安装 / 发现新
版本 vX / 已经是最新)+ 本机版本与最新可用两个事实 + 主按钮;「重装 / 指定版本 /
允许无校验值 / 回滚 / 兼容区间」全部收进折叠的「更多安装选项」。
-
卸载整卡默认折叠,两个危险按钮不再常驻页面。
-
每个分区加一句职责说明;顺序调整为「服务 → 平台登录 → 账号总览 → 授权 → 设置 → 日志」。
-
平台登录页新增"登录之后能干什么"小表(设备 / 摄像头 / 场景与定时 / 自动化都在聊天里
让模型调工具完成),并保留凭据只走本机回环的说明。
-
授权页免费版文案修正:免费版不再显示 —(永久或未订阅) 与
— · 还剩 0 天,改为 无限期(免费版) / 无限期(免费版不涉及);米家永久免费
不再显示"试用剩 0 天"。根因是子进程用空字符串表示"没有",而旧代码用 ?? 兜底
挡不住空串;现在 src/child-api.ts 把空值一律归一为 null,并且宽限期没有到期时间时
不再把 grace_period_remaining = 0 当成"还剩 0 天"。
⚠ 注意 pnpm 的发布冷静期:pnpm 11 默认 minimum-release-age = 24h,
刚发布的版本用 dsh plugin add dsh-feyagate 可能仍装到上一个版本(会被静默降级,
并往 pnpm-workspace.yaml 写 minimumReleaseAgeExclude)。要立刻装到 0.3.5,
请写全版本号:dsh plugin --profile <profile> add dsh-feyagate@0.3.5。
0.3.4(历史):Windows 上「安装后台服务」必然失败。0.3.3 的清单里
channel.stable["win-x64"] 指向 v1.2.19,而该版本上游只发了 zip、没有
.sha256 sidecar,清单只能记 sha256: null;安装流程的设计是"没有校验值一律拒装",
于是 Windows 用户点按钮只会看到「上游未为 … 提供校验值」。0.3.4 把 win-x64 的默认
版本改成 v1.2.21,并修掉让这个坏清单通过 CI 的原因:verify-manifest.mjs 现在
按运行时同一套排序取真正会被下载的资产,并把"默认装不上"从 warning 升级为错误,
另加 scripts/check-manifest-guard.mjs 用真清单钉死这条规则。
0.3.1 变更(历史):重新生成 manifest/server-manifest.json。上游 miloco-mcp-server v1.2.20 的 mac-arm64 产物
已于 2026-09-25 重新打包并覆盖发布(旧包缺 4 个依赖库导致启动即 dyld: Library not loaded,且 MQTT 未编入
导致华为设备能登录却无法控制)。清单里的 sha256 必须跟着更新,否则安装会以校验不通过结束 ——
请先升级插件到 0.3.1 再安装/升级后台服务。
一句话
FeyaGate(飞阳网关)的 DSH 插件:在 DSH 里托管本机的后台服务 miloco-mcp-server(自动下载 → 校验 → 安装 → 守护 → 升级 → 失败回滚),把它的 76 个 MCP 工具(数量出自 FeyaGate_MCP_API.md §4)接进 DSH,并在设置面板里提供「服务 / 账号 / 授权」三个管理界面。
插件本体开源(MIT);真正的智能家居逻辑在闭源二进制里,二进制不进这个仓库、不随 npm 包分发,运行时从权利人自己的渠道下载并按清单校验(见 NOTICE)。
用词约定(全文一致):平台账号相关一律说「登录」,只有许可证才说「授权」;本机这个进程一律叫「后台服务」,不叫「网关」;「飞阳网关」是产品名与设置入口名。
架构一眼看完
┌─────────────────────────────── DSH 宿主进程 ────────────────────────────────┐
│ │
│ ┌────────────────────────┐ ┌─────────────────────────────────────┐ │
│ │ 浏览器半侧 lib/client.js│ 同源 │ Host 半侧(插件本体,进程内运行) │ │
│ │ 设置 › 飞阳网关 │ ◄────► │ /dsh-feyagate/* 管理 API │ │
│ │ [服务][账号][授权] │ HTTP │ 安装·校验·守护·升级·生成 config.yaml │ │
│ └────────────────────────┘ └──────┬──────────────────┬───────────┘ │
│ │ ① spawn / 健康轮询 │ ② 常驻 │
│ ┌───────────────────────────────┐ │ /health │ │
│ │ @deepseek-ai/dsh-mcp-client │ HTTP │ ▼ │
│ │ serverName: feyagate │ ─────► │ ┌────────────────────┐ │
│ │ 工具名 mcp__feyagate__* │ │ │ MCP 门面 :38081 │ │
│ └───────────────────────────────┘ │ │ 秒答 initialize │ │
│ ▲ 模型在对话里调用 │ │ 就绪后代理工具调用 │ │
│ │ │ └─────────┬──────────┘ │
└─────────┼──────────────────────────────────┼──────────────────┼────────────┘
│ ▼ │ ③ 代理
│ ┌──────────────────────────────┐ │
│ │ 后台服务 miloco-mcp-server │◄─┘
│ │ 127.0.0.1:38080(可漂移) │
│ │ POST /mcp/http · GET /health │
│ │ 落盘 ~/.dsh/dsh-feyagate/ │
│ └───────────┬──────────────────┘
│ │ HTTPS / P2P / MQTT
│ ▼
│ 小米云 · 涂鸦云 · 美的云 · 易微联 · 华为云 · Home Assistant
│ ▲
│ │ 局域网
└─────────────── 米家摄像头 · 小爱音箱 · 小智终端
三条要点:
- 桥连的是插件自己的常驻门面,不是子进程。门面端口写进
state.json,由 cordis.patch.yml 里的 !!js 表达式在 boot 时读取;子进程没装也能秒回 initialize(只返回空工具表),因此"先启动 DSH 再装服务"不会烧掉 MCP 桥的重连预算,升级窗口对模型也是零感知。tools/list_changed 的送达是最终一致的:桥的 SSE 流建立时若子进程已健康,门面立即补发一次;通知发出时若暂无客户端流,则标记 pending 并有界重发(5s/次、上限 12 次)——两种时序下桥都不需要重启 DSH 即可拿到工具列表。
- 子进程真实端口是运行期细节。默认
38080,被占用时在 20 个端口范围内漂移;期望端口进 state.json 的 server.port,实际生效的端口在启动这一轮写进生成的 config.yaml,不写进任何静态配置。
- 安装根目录本身是不可移动的。
config.yaml 就放在根目录,子进程把设备身份与许可证锚定在 config.yaml 所在目录 —— 根目录一换就等于换了一份身份(见下方「数据在哪里」与 ADR-0002)。
安装与最短路径
1. 装插件
dsh plugin --profile <profile> add dsh-feyagate
<profile> 是你要装进去的 profile 名(例如 web)。这条命令把参数原样转发给 pnpm,并在 package.json 里看到 dsh.bundle.patch 后把本插件的 bundle 追加进 dsh.profile.bundles —— 不需要手改 profile 文件。装完重启(或让 HMR 重合成配置层)后,设置面板左侧会多出一行「飞阳网关」。
2. 装后台服务(唯一必须做的一件事)
打开 设置 › 飞阳网关 › 服务 › 概览,点 安装后台服务。安装向导会依次做五件事,每一步都有真实进度:
① 下载 ████████░░░░ 56% 17.1 MB / 30.4 MB · 1.8 MB/s
② 校验 sha256 与同版本 .sha256 比对 ← 不通过就删除文件并中止
③ 解压 释放到 versions/<version>/
④ 写入 可执行权限(macOS 还会去 quarantine + ad-hoc 重签)
⑤ 启动 切 current 指针 → 拉起进程 → 轮询 /health
MVP 不会在你点之前偷偷下载任何东西。没装就是没装,界面只会给你一个按钮。
3. 确认工具接上了
安装完成后,在对话里问一句「列出我的智能家居设备」。模型应该能调用到形如 mcp__feyagate__* 的工具(例如 mcp__feyagate__device_list)。
公开工具名由 MCP 桥做函数名规范化(原始名里有 /,如 device/list),以运行时 tools/list 的结果为准;本仓库文档里写的名字是形状示意。
4. 登录平台账号
在设置界面里点着登录:设置 › 插件 › 飞阳网关 › 平台登录,米家 / 涂鸦 / 美的 / 易微联 / 华为五张卡片各自一个流程(涂鸦直接显示可扫的二维码,米家是「拿授权地址 → 粘回回调地址」两步)。哪张卡片能用由后台服务自己报的能力决定:本机 v1.2.19 没有华为工具,华为卡片就明确写「本构建不支持」而不是给一个必然失败的按钮;后台服务升到 v1.2.20 后这张卡片自己出现,插件不用跟着升级。
凭据(账号、密码、验证码)只在本机回环内交给后台服务进程:插件不落盘、不写日志、不回显,写操作全部校验 Origin,密码输入框提交后即清空。详见 ADR-0009。
对话里也能走(工具清单见 FeyaGate_MCP_API.md)——两者调的是同一批工具:小米走浏览器 OAuth、涂鸦走二维码、美的/易微联走账号密码、华为走两步验证码。
涂鸦(唯一一条完全在聊天里走完的授权链) —— 你只需要做两件事:
- 先在涂鸦 App 里查一次自己的用户代码:我的 → 设置 → 账号与安全 → 用户代码,把它发给模型(形如
AY1790336520…)。
- 模型回复里会出现一张二维码图片(不是 token 文本)。用涂鸦 App 右上角「+ / 扫一扫」扫它,再在 App 里点「确认登录」。
之后不用你再说话:插件在服务端替模型轮询(单次最多等 35 秒),扫到即返回,模型会告诉你已登录,并可以接着 device_list 看设备。
为什么必须由插件出图:上游 auth/tuya_qr 只回一个 token(tuyaSmart--qrLogin/?token=…)——那是给摄像头扫的载荷,聊天里没有可扫的东西,用户会被卡在「请扫描下方二维码」却没有二维码。插件把 token 渲染成 PNG(/dsh-feyagate/auth/tuya/qr.png?token=…)并写进工具结果与工具描述,见 docs/verify-tuya-auth-flow.md。
图片没显示时(客户端不渲染图片):工具结果里同时给了 qr_text_url(方块字符版二维码,任何客户端都能显示);也可以直接把该地址粘进浏览器。
最短路径到"控制一盏灯":装插件 → 装后台服务 → 对话里让模型登录米家 → 让模型 device/list 找到灯 → 开灯。
功能清单
MVP(0.1.x 目标)
| 能力 | 说明 |
|---|
| 后台服务托管 | 按 manifest/server-manifest.json 解析平台资产 → 多源降级下载 → 先校验后解压 → 落到 versions/<version>/ → 切 current 指针 |
| 进程守护 | spawn / PID 文件 / 孤儿与多实例清理 / /health 轮询(40 × 500ms)/ 崩溃退避重启(1s→30s)/ 熔断(60 秒内 5 次崩溃停止自动重启) |
| 端口管理 | 默认 38080,占用时在 20 个端口内漂移;期望端口留在 state.json 的 server.port,本次实际端口记进 server.effectivePort 并写进生成的 config.yaml |
| config.yaml 生成 | 插件是唯一写者,字段归属见 ADR-0003;子进程会回写的字段读回合并,不覆盖 |
| MCP 接入 | 官方 @deepseek-ai/dsh-mcp-client 桥指向插件常驻门面(127.0.0.1:38081),工具以 mcp__feyagate__* 出现;tools/list_changed 最终送达(流建立补发 + pending 有界重发),装服务/改端口后桥无需重启 DSH |
76 个 MCP 工具(数量出自 FeyaGate_MCP_API.md §4 的自述) | 设备/摄像头/场景/定时/触发规则/记忆/技能/小智/授权查询,全部经门面代理到子进程 |
| 设置界面 · 服务 | 概览 / 安装与重装 / 升级与版本 / 端口与网络 / 日志与诊断 / 危险区 |
| 设置界面 · 授权 | 许可证与试用期:版本、状态、到期、宽限、授权码写入、设备 ID、各平台试用剩余 |
| 设置界面 · 账号总览 | 只读:平台清单、登录状态、设备数、试用剩余(清单以服务端下发为准,本地不写死) |
| 设置界面 · 平台登录 | 五个平台的图形化登录/退出:米家两步 OAuth(自动从回调地址里抠 code)、涂鸦二维码(服务端完成最长 35 秒的扫码等待)、美的/易微联账号口令、华为两步验证码(按 retry_without_code 分流,不白费一个验证码);能力协商驱动,缺哪个平台就说清缺什么;授权过期时给出"去授权标签"而不是"密码错了" |
| 升级与回滚 | 检查更新 → 升级到 versions/<new>/ → 健康确认失败自动回滚 lastKnownGood;断电安全靠 pending 标记 |
| 随插件分发的 Skill 文档 | skills/feyagate-gateway/SKILL.md,教模型怎么正确调用这 76 个工具 |
| 插件自身升级 | 只做"检测到新版本 + 提示 + 给可复制的升级命令"(见下) |
二期(明确不是"已支持")
- 常驻门面的工具集热更新(子进程就绪后发
notifications/tools/list_changed)之外的多端点管理:局域网 ESP32 网关、远端 miloco,条目分「本机托管」与「远端只连」。
- Home Assistant(上游
miloco-mcp-server 与桌面端都没有 HA provider,只有 ESP32 固件有;要做只能直连 ESP32 的 /api/v1/platform/ha/* 或 HA 自己的 WebSocket)。
- 设备页与摄像头页(连接 / 断开 / 抓拍 / 采集参数)、智能页(小智 AI / Vision AI / 触发规则 / 记忆 / 技能)。
- 插件内一键升级(spawn
dsh plugin update + 宿主重启)。
- 诊断包导出、镜像源配置界面。
明确不做
- 不做自研 DRM:许可证事实源在子进程,插件只呈现与转发。
- 不读写已有的
~/.feyagate 安装(只做只读探测,用于提示"检测到既有安装",见 ADR-0002)。
- 不在运行期动态增减工具集(会打断 KV cache 前缀;平台开关要求重连)。
- 不下载、不镜像、不分发小米专有库
libmiot_camera_lite。
设置界面
界面注册在唯一一个 settings.section(id feyagate,label「飞阳网关」,order 30)下;插件级参数挂在 settings.plugin.item,可复用 DSH 通用的「已覆盖 / 恢复默认 / 未保存 / 保存失败」表单语义。参数表(src/settings.ts 的 PluginSettings):
| 参数 | 默认 | 说明 |
|---|
serverPort | 38080 | 子进程的期望端口 |
facadePort | 38081 | 门面端口(改它需要重启 DSH) |
bindAddress | 127.0.0.1 | 可选 0.0.0.0(开局域网,必须看警告) |
cloudServer | cn | 小米云区域 |
autoStart | true | DSH 启动时自动拉起子进程 |
mirrorBase | null | 自建镜像前缀(降级链的第三环) |
localArchive | null | 本地安装包路径(降级链的最后一环) |
allowUnverified | false | 允许安装"完全没有校验值"的资产(界面里是显式确认) |
安装根目录不在参数里:它固定是 ~/.dsh/dsh-feyagate(或 $DSH_HOME/dsh-feyagate)。这不是漏了,是因为换目录等于换设备身份。
地图不是按钮:DSH 的 settings.section 只拿到 close(),拿不到跨页跳转能力,所以界面里的跨页指引一律写成可复制的文字路径(设置 › 插件 › 插件配置 › 飞阳网关)。
| Tab | MVP 做到哪 | 解决什么问题 |
|---|
| 服务 | 概览 / 安装与重装 / 升级与版本 / 端口与网络 / 日志与诊断 / 危险区 | 「后台服务在不在、什么版本、坏在哪、怎么修」 |
| 账号总览 | 只读总览(平台清单 + 登录状态 + 设备数 + 试用剩余) | 「一屏看清哪个平台是红的」 |
| 平台登录 | 五个平台的登录/退出(能力协商驱动,缺什么说什么) | 「直接在界面里把账号登进去」,不必先学会跟模型描述工具调用 |
| 设备 | ❌ 二期 | 「设备都被看见了吗、摄像头能看吗」 |
| 智能 | ❌ 二期 | 小智 AI / Vision AI / 触发规则 / 记忆 / 技能 |
| 授权 | 许可证与试用期(授权码写入、各平台状态、设备 ID) | 「免费能用什么、授权解锁什么、还剩几天」 |
每个 tab 顶部有一条常驻服务状态条(10 秒轮询、页面不可见时暂停),把运维面的结论带到每一页:
● 运行中 · v1.2.20 · 端口 38080 · 3/7 平台已登录 · 试用剩 12 天 [检查更新]
○ 未安装 · 尚未下载后台服务 [安装]
▲ 降级运行 · 华为设备控制不可用(命令通道未建立) 查看原因
截图占位(docs/images/ 目录本仓库暂不创建,避免提交二进制;下面按顺序补图):
界面设计规格(逐屏字段、状态词表、错误文案三层结构、可用性走查、验收清单)在 UX 设计文档;本文只描述已确认的 MVP 范围,两者不一致处见下方「已知不一致」。
为什么需要 manifest/server-manifest.json
因为上游资产命名逐版本漂移,运行时拼文件名必然失败。这不是假设,是同一仓库同一年的实测事实:
| 版本 | 平台 | 真实资产名 | 备注 |
|---|
| v1.2.20 | mac-arm64 | miloco-mcp-server-mac-arm64-v1.2.20.zip | 「规范」形态 |
| v1.2.20 | mac-x64 | miloco-mcp-server-mac-x64-v1.2.20.tar.gz | 完全没有 zip |
| v1.2.20 | win-x64 | —— | 这个版本没有任何 Windows 资产,所以 Windows 默认目标停在 1.2.19 |
| v1.2.19 | win-x64 | miloco-mcp-server-1.2.19-windows-x64.zip | 版本号在前、写 windows 而不是 win |
| v1.2.19 | win-x64 | miloco-mcp-server.exe | 还有一个裸可执行文件,没有压缩包 |
| v1.2.17 | linux-arm64 | miloco-mcp-server-linux-arm64-v1.2.17.tar.gz | linux-arm64 的最新版本停在 1.2.17 |
所以本仓库自带一份权威清单:逐平台记录真实文件名、体积、URL 与 sha256(没有 .sha256 sidecar 的资产记 null)。清单同时承载:
pluginCompat.minSupportedServer / maxTestedServer:低于下限拒绝运行,高于上限提示"未测试"。
channel.stable:逐平台的默认目标版本(Windows 与 linux-arm64 因此不会去追一个根本不存在的版本)。
fotaType:FOTA 兜底源里各平台对应的 type id(mac-arm64 与 linux-arm64 没有 FOTA 条目,实测)。
维护者更新清单:
node scripts/gen-manifest.mjs # 抓取全部 release,重算 sha256
node scripts/gen-manifest.mjs --tag v1.2.20 # 只更新一个 tag
node scripts/gen-manifest.mjs --compute-missing # 对没有 sidecar 的资产下载一次并本地算 sha256
# (默认只补每个平台的默认目标)
--compute-missing 的意义:sha256: null 的资产必须先由用户显式确认才能安装。它对"默认目标"把这条路走通,更早的钉版本仍然留在不可校验状态,因此不在 happy path 上。
package.json 里另有 npm run check:manifest(→ scripts/verify-manifest.mjs)用于校验清单自洽(资产 URL 与文件名一致、channel 指向存在的资产、pluginCompat 区间合法),以及 npm run check:manifest-guard(→ scripts/check-manifest-guard.mjs)用真清单做底稿的自检。两条都是硬门禁:channel.stable[平台] 指向的版本,其"运行时会真正下载的那个资产"必须有 sha256 —— 否则该平台点「安装后台服务」必然被校验门拒绝(0.3.3 在 Windows 上就是这样翻车的)。适合接进 CI。
运维手册
数据在哪里
安装根目录默认 ~/.dsh/dsh-feyagate($DSH_HOME 存在时是 $DSH_HOME/dsh-feyagate):
~/.dsh/dsh-feyagate/
├── state.json 插件唯一的可变账本:current / lastKnownGood / pending / 端口 / facade 端口 / 设置
├── config.yaml ★ 生成物(插件是唯一写者),子进程按 `--config` 读它
├── data/ ★ 子进程数据:token、license.json、device_id.txt、memory、skills、快照
├── logs/server.log 子进程 stdout/stderr + 插件日志,单行截断 4000 字符
├── versions/<version>/ 每个版本一个不可变目录(下载→校验→解压的落点)
├── cache/ 下载的压缩包与解压暂存
└── current 指针文件(不是软链接,为了 Windows),内容是一行版本号
config.yaml 为什么在根目录而不是 config/ 子目录里:这不是风格选择。子进程把所有相对路径按 config.yaml 所在目录展开(src/config.cpp 的 resolve_path 作用于 license_file、每个 *_token_file、memory.data_dir、skill.*_dir),并且 src/main.cpp 用它推导自己的数据目录(device_id.txt、token_usage.json)。所以"config.yaml 旁边那个目录"就是数据根。放进 config/ 会让 data/ 悄悄变成 config/data/,还会挪走 license.json —— 那正是设备 ID 与授权绑定的地方,用户升级一次插件就会被云端当成新设备、重新领一份免费试用。这就是 src/paths.ts 把文件放在根目录的原因。
唯一不可移动的东西就是安装根目录。移动或重建它 = 换一份 device_id = 授权与免费试用重新绑定。因此插件从不"迁移"这个目录,只往里写。备份时请整目录一起备。
改端口
- HTTP 端口(子进程):设置 › 飞阳网关 › 服务 › 端口与网络 → 保存 → 会提示"端口改动需要重启后台服务才会生效",并自动重启。默认
38080,被占用时自动漂移并在界面上显示真实端口。
- 门面端口:默认
38081,写进 state.json。改动它需要重启 DSH,因为 MCP 桥的 URL 来自 boot 时求值的 cordis.patch.yml。
- 绑定地址:默认只绑 loopback。要开局域网必须显式开启,界面会同时给出"本地接口不校验身份"的警告。
日志
- 界面:服务 › 日志与诊断,可切级别(对应子进程的
LOGV/LOGD/LOGI/LOGW/LOGE)、按行数与关键字过滤、复制全部。
- 文件:
~/.dsh/dsh-feyagate/logs/server.log(同一份环形缓冲区同时喂界面与文件,所以界面里能看到"崩溃行 + 守护进程的反应"的因果顺序)。
回滚
三种情形:
- 升级后没通过健康检查 → 自动回滚到
lastKnownGood,界面留一条常驻橙条,提供「重试升级 / 查看失败日志 / 保留旧版本并忽略此版本」。
- 升级中途断电/崩溃 →
state.json 里的 pending 在下次启动仍未确认,自动回滚。这就是 versions/ 不可变 + current 指针 + pending 三件套存在的理由。
- 手动回滚 → 服务 › 升级与版本 › 版本历史,点「回滚到此版本」(本机已有该版本目录时才可用;没有则需要重新下载)。
卸载
先停服务(服务 › 危险区 › 停止服务),再删目录:
# 1) 停服务(界面:设置 › 飞阳网关 › 服务 › 危险区 › 停止服务)
# 2) 删后台服务与数据(不可恢复:平台凭据、设备、记忆、技能、快照、日志)
rm -rf ~/.dsh/dsh-feyagate
# 3) 卸载插件本体
dsh plugin --profile <profile> remove dsh-feyagate
只想留数据、只删二进制?删 ~/.dsh/dsh-feyagate/versions 与 current 即可(界面上的「卸载服务」做的就是这件事,凭据与配置保留)。
清理时不要动根目录以外的东西:不要删 config.yaml 与 data/(那是设备身份与授权),也不要动 ~/.dsh/profiles/<profile>/(那是 DSH 自己的)。只想留数据、只删二进制?删 ~/.dsh/dsh-feyagate/versions 与 current 即可(界面上的「卸载服务」做的就是这件事,凭据与配置保留)。
故障排查
| 症状 | 可能原因 | 处理 |
|---|
启动时提示 feyagate-gateway … did not activate / failed to import | 装的是源码目录或 git 依赖,而 lib/ 是构建产物、不在仓库里;宿主 import 不到入口 | 在该包目录跑 npm install && npm run build(prepare 脚本会在 npm install 时自动构建;dsh plugin add <本地路径> 用 link: 语义、不会触发它)。npm 安装的正式包自带 lib/,不受影响 |
工具列表里没有 mcp__feyagate__* | ① 插件没装进当前 profile;② MCP 桥行没生效;③ 桥连不上门面(门面端口被改但没有重启 DSH);④ 后台服务尚未装/尚未健康(门面如实回空表);⑤ 上游工具清单不合规:零参数工具的 inputSchema 是 {}(缺 type:"object"),严格客户端会整份拒收 tools/list ⇒ 一个工具都注册不上(服务却显示"已连接") | ① dsh plugin --profile <p> why @dsh-external/dsh-feyagate-gateway;② 检查 profile 的 bundle 列表里有没有本插件;③ 设置 › 飞阳网关 › 服务 看状态条,重启 DSH 让 patch 重新求值;④ 装/启动后台服务后无需重启 DSH:服务健康后门面自动(必要时补发)tools/list_changed,桥随即 re-sync 出工具;⑤ 已修:门面转发 tools/list 时会把不合规的 inputSchema 补齐成 {type:"object", properties:{}}(见 docs/verify-mcp-inputschema-fix.md),升级插件后重启 DSH 即可。判断方法:插件的 logs 里会有一行 已修正 N 个工具的 inputSchema |
| 工具列表里有工具但调用失败 | 后台服务没起 / 正在升级 / 门面还没代理到子进程 | 看状态条:未安装 → 点安装;异常 → 看日志;升级中 → 等 10–20 秒 |
| 服务起不来:端口被占用 | HTTP 端口被别的进程占了(常见:另一份 miloco / 既有 ~/.feyagate 安装) | 界面报 FG-PORT-001 并给出占用 PID;点「换一个端口」(插件会在 20 个端口内漂移)或结束占用进程 |
| 服务起不来:macOS 未签名被拦 | Gatekeeper 拦下未公证的第三方二进制 | 界面报 FG-PERM-001,给两条路:系统设置 › 隐私与安全性 ›「仍要打开」,或复制 xattr -dr com.apple.quarantine <安装目录>/versions/<ver>/miloco-mcp-server。受 MDM 管理的机器绕不过去 |
| 服务起不来:依赖缺失 | 上游产物漏带运行库。已实测:mac-arm64 v1.2.20 的二进制声明依赖 lib/libyaml-cpp.0.9.dylib、libavcodec.62.dylib、libavutil.60.dylib、libswscale.9.dylib,而归档里只有 libmiot_camera_lite.dylib —— dyld 立即中止,进程活不过 20 毫秒 |
完整错误码表(FG-<域>-<三位号>,域为 NET / PKG / PERM / PORT / BOOT / PLAT / HW / LIC / HOST / API)见 docs/user-guide.zh.md。
开源与合规边界
完整口径见 NOTICE,三条红线:
- 仓库不含任何二进制。不收
miloco-mcp-server 的可执行文件、压缩包或它的一部分 —— 仓库里没有,npm 包里没有,任何 release 附件里也没有。运行时从权利人自己运营的渠道下载,落到用户自己的机器上。
- 二进制的许可由它自己的渠道决定,与本项目的 MIT 无关。想再分发二进制、或想运营镜像的人,必须自行满足权利人的条款。社区 fork 必须不要以本项目名义发布二进制镜像;下载源可配置,fork 应指向自己的渠道。
- 小米专有库
libmiot_camera_lite 永不分发。不下载、不镜像、不捆绑、不再分发。上游部分构建不含它,此时摄像头不可用、其它平台正常,插件把这种情况显示为"降级运行 + 原因",而不是静默失败或假装功能存在。
一处诚实的未知:miloco-mcp-server 的源码树带 MIT 许可证文件,README 又写明摄像头原生库是小米专有、"闭源"是分发策略而非许可证结论;其自述基于 Xiaomi Miloco 项目,上游血统的许可证未在本项目内审计。产物若静态/动态链接了 LGPL 组件(例如 Linux 上的 FFmpeg 系列),再分发需满足相应义务 —— 这些义务属于发布产物的人,本项目不做审计。若你是权利人并希望更正此处表述,请开 issue。
贡献指南
开发环境
- Node.js ≥ 20.16.0(
engines.node 与 engines.dsh >= 0.1.5-rc.1)。
- 全新克隆的入口只有一条命令:
npm install && npm test。lib/ 是构建产物、不在仓库里,所以下面那些依赖 lib/ 的检查必须先 npm run build;npm test 已经包含构建。
- 构建:
npm run build(= bash scripts/build.sh,产出 lib/index.js 与 lib/client.js)。也可分开跑:npm run build:client(tsdown 打浏览器半侧 lazy-CJS)、npm run typecheck(tsc 只检查不产出)。
- 静态检查(不需要构建,改完随手就能跑):
npm run check = typecheck + check:manifest + check:manifest-guard + check:patch。
- 需要构建的检查:
npm run check:runtime = 下载层自检 + 离线冒烟(smoke.mjs --offline)+ 属主/看门狗(check-supervise-ownership.mjs)+ 涂鸦二维码(check-tuya-qr.mjs)+ 平台授权(check-platform-auth.mjs)。
- 完整验收:
npm test = build + check + check:runtime + smoke:install,共 237 项计数断言(24 补丁格式 + 5 下载层 + 19 离线冒烟 + 10 属主/看门狗 + 26 涂鸦二维码 + 54 平台授权 + 99 安装生命周期),另有类型检查与清单结构校验。安装生命周期覆盖:装 / 升级 / 回滚 / 校验不符拒装 / 无校验值默认拒装 / 真重装 / SIGKILL 自愈 / 接管未就绪进程 / 卸载留数据 / 启动即失败快速失败 + 失败不留脏状态 + 同版本重试真的重试。
- 清单维护:
npm run manifest(重新生成)、npm run check:manifest(CI 校验 + 校验值门禁)、npm run check:manifest-guard(门禁自身的自检)。
- 端到端冒烟:
npm run smoke(默认会真的去 GitHub 下载;加 --offline 跳过)。
冒烟脚本会往 .smoke/(smoke-install.mjs 用 .smoke-install/)写下载物与状态,两者都已在 .gitignore 里。
浏览器半侧有一个必须遵守的契约:产出必须是 window.__ModuleLoader__.load({ id, factory }) 形态的 lazy-CJS(tsdown.config.ts 的 banner/footer 已经在做这件事)。只有前端 baseline 白名单里的模块可以留成裸 require(...),其余必须打进 bundle;额外需要的前端包要写进 package.json 的 dsh.client.inject。
目录结构
app/dsh-feyagate/
├── package.json exports["."](Host)/ exports["./client"](浏览器半侧)、dsh.bundle.patch、dsh.client
├── cordis.patch.yml ★ 插入两行:feyagate-gateway(本体)+ feyagate-gateway-mcp(官方 MCP 桥)
├── tsconfig.json Host 半侧 tsc:src/ → lib/
├── tsdown.config.ts 浏览器半侧 lazy-CJS bundle → lib/client.js
├── LICENSE / NOTICE MIT / 合规口径(不含二进制、专有库永不分发)
├── manifest/server-manifest.json ★ 权威清单:逐平台真实文件名 + sha256 + 兼容区间 + 默认通道
├── scripts/gen-manifest.mjs ★ 唯一维护清单的入口(见「为什么需要清单」)
├── scripts/build.sh tsc + tsdown 串起来,产出 lib/
├── scripts/verify-manifest.mjs CI 校验清单自洽 + 默认版本必须可校验
├── scripts/check-manifest-guard.mjs 上面那条门禁的自检(合成形状 + 真清单底稿)
├── scripts/manifest-rules.mjs 清单规则本体(CLI 与自检共用同一份;**不要放进 scripts/lib/**,
│ `.gitignore` 的 `lib/` 会把它一起忽略掉)
├── scripts/check-tuya-qr.mjs 涂鸦二维码:编码后用独立解码器读回
├── scripts/check-platform-auth.mjs 平台授权:能力协商 / 凭据边界 / 门禁与分流
├── scripts/smoke.mjs 端到端冒烟:真下载 → 真校验 → 真解压 → 真启动
├── skills/feyagate-gateway/SKILL.md 随插件分发的模型技能文档
├── src/ Host 半侧
│ ├── constants.ts 端口/路径/协议路径/退避参数,全部常量集中在此
│ ├── types.ts 契约:manifest、state、RuntimeStatus、JobSnapshot、API 信封
│ ├── paths.ts 落盘布局与 `current` 指针读写
│ ├── state.ts state.json 原子写 + `pending` 握手 + 指针兜底
│ ├── log.ts 环形日志缓冲(喂界面与文件)
│ ├── util/atomic.ts tmp+rename 原子写
│ ├── util/platform.ts 平台标签映射、版本比较、二进制名
│ ├── manifest.ts 清单解析、按平台解析资产、兼容区间判定
│ ├── config-gen.ts config.yaml 唯一写者:MANAGED_FIELDS / SEED_ONLY_FIELDS
│ ├── settings.ts 插件参数(PluginSettings)读写与校验
│ ├── launcher.ts 子进程命令行与工作目录
│ ├── supervise/process.ts spawn / PID / 孤儿清理 / 优雅停止
│ ├── supervise/health.ts 就绪探测、`/health` 轮询、空闲端口探测
│ ├── runtime.ts 状态机:not-installed → … → running / degraded / error
│ ├── install.ts 安装 / 升级 / 回滚 / 卸载,全部表达为 job
│ ├── jobs.ts 长任务账本(phase / percent / attemptedSources / noop)
│ ├── download/sources.ts 多源降级链:github → fota → mirror → local
│ ├── download/fetch.ts 带进度与取消的下载
│ ├── download/verify.ts 流式 sha256 / md5
│ ├── download/extract.ts zip / tar.gz / 裸 exe,拍平 + chmod 755 + macOS 去隔离
│ ├── child-api.ts 子进程 REST / MCP 调用封装
│ ├── api.ts 同源管理 API `/dsh-feyagate/*`(写操作校验 Origin)
│ ├── mcp/facade.ts 常驻门面:恒答 initialize、空表降级、就绪后代理
│ ├── download/index.ts 下载子模块的统一出口
│ ├── index.ts 插件入口:Config schema、apply()、ctx.effect 清理
│ └── client/ 浏览器半侧
│ ├── index.tsx 设置面板:服务 / 账号 / 授权(React)
│ └── contract.ts Host ↔ 浏览器半侧共享的类型契约
└── docs/
├── design.md 工程内设计文档(双半侧边界、MCP 选型、状态机、字段归属)
├── user-guide.zh.md 面向最终用户的操作手册
├── verify-mcp-inputschema-fix.md
├── verify-platform-login.md
├── verify-tuya-auth-flow.md
│ 验证记录:AI 看不到工具的真实根因(上游 inputSchema 不合规)与修复
└── adr/0001…0008 8 条架构决策记录(0008:多实例共享安装根时的接管策略)
每个脚本做什么
| 脚本 | 作用 |
|---|
scripts/gen-manifest.mjs | 抓 GitHub Releases,逐资产识别平台与打包类型,抓 .sha256 sidecar,算出逐平台默认版本,写出 manifest/server-manifest.json。支持 --tag 与 --compute-missing |
scripts/build.sh | 先 tsc -p tsconfig.json 编 Host(含 .d.ts → lib/types/),再 tsdown 打浏览器半侧 |
scripts/verify-manifest.mjs | CI 校验:清单自洽(资产 URL 与文件名一致、channel 指向存在的资产、pluginCompat 区间合法),并按运行时同一套排序确认默认版本的主资产带 sha256(没有就退出码 1)。可带一个可选参数检查历史清单:node scripts/verify-manifest.mjs <path> |
scripts/check-manifest-guard.mjs | 上面那条"默认版本必须可校验"规则的自检:用真清单做底稿,分别构造"默认版本无校验值"与"可校验资产排在数组前面、不可校验的 zip 在后面"两种形状,断言前者报错、后者也报错(证明判定跟运行时排序走而不是 assets[0]) |
scripts/manifest-rules.mjs | 清单规则本体(validateManifest / pickPrimaryAsset / assetRank)。CLI 与自检共用这一份,避免"规则改了但门禁还是老规则" |
scripts/selfcheck-download.mjs | 下载/校验/解压层的单元自检(用构造出来的归档,不联网):校验失败必须删掉坏包且绝不落位、无校验值默认拒绝、顶层目录上移、找不到二进制时报错而不是静默成功 |
scripts/check-patch.mjs | 校验 cordis.patch.yml:用与 DSH 完全相同的解析方式(parseDocument + tag:yaml.org,2002:js)解析,并实际执行 url 表达式,确认它能读到 state.json 的漂移端口 |
scripts/check-tuya-qr.mjs | 涂鸦授权:token→二维码(PNG 与文本兜底都用独立解码器 jsqr 读回,不只看「产生了图片」)、chat_display 文案、路由端到端、非法 token 400(26 项断言) |
scripts/check-platform-auth.mjs | 平台授权(插件 API 边界 + 假子进程):能力协商(缺华为时卡片就该缺席)、涂鸦扫码一次请求等到 authorized(上游被问 ≥2 次)、用户代码错不伪造二维码、美的/易微联/华为的登录与失败文案、米家「粘整段回调地址 → 抠出 code」,授权门禁 capability_denied → 403 并指向授权标签、华为 retry_without_code 不当成失败、退出登录 501 如实说上游没有、密码不出现在插件日志/状态/任何响应里、其他 Origin 的登录请求 403、服务未运行时 503(54 项断言) |
scripts/check-supervise-ownership.mjs | 属主与看门狗:pid 文件写明属主时访客不得杀掉别人的子进程(只如实报告),属主消失后才允许替换,无人拥有的会被接管并登记自己为属主(10 项断言,用快速看门狗参数,约 3 秒) |
改代码前请先读
提交约定
- 术语必须与 UX 文档 §7.3 的唯一推荐用词一致(平台侧「登录」、许可证侧「授权」)。
- 本机那个进程有两个名字,按场合分开用,别混:
- 界面上叫「FeyaGate MCP 引擎」(
ENGINE_DISPLAY_NAME)—— 用户不需要认识上游二进制名;
- 代码与 ADR/内部文档里仍叫「后台服务」(supervisor、child、
miloco-mcp-server 等),
因为那里说的是进程与文件系统事实。下载文件名、镜像前缀等事实性文案照实写。
- 不确定的事实写成"未核实/待确认",不要写成断言。文档里的每个端口、路径、命令都必须能在本仓库或引用文档里找到出处。
实现状态(诚实声明)
本 README 描述的是 0.3.6 的形态(dsh-feyagate 已发布到 npm),src/ 与 scripts/ 里的文件都已落地并可构建:
- ✅ 双半侧都已在源码里:Host(
index.ts、runtime.ts、install.ts、supervise/*、mcp/facade.ts、api.ts、config-gen.ts、download/*、child-api.ts、settings.ts)与浏览器半侧(client/index.tsx + client/contract.ts)。
- ✅ 工具链齐全:
scripts/{build.sh,gen-manifest.mjs,verify-manifest.mjs,check-manifest-guard.mjs,check-patch.mjs,smoke.mjs,smoke-install.mjs},lib/ 是构建产物(已 gitignore)。
- ⏳ 尚未做的不是代码,而是发布侧的事:CI 接线、在装有真实上游引擎的机器上跑一次完整验收、设置界面的截图(本文里还是
TODO(screenshot) 占位)。
已经实测过的部分
npm test 全绿(24 + 5 + 19 + 10 + 26 + 54 + 99 项断言,另有清单结构校验)。具体覆盖:
| 验证对象 | 证据 |
|---|
patch 能被 DSH 解析,且 !!js 表达式在 boot 时读到 state.json 的漂移端口 | scripts/check-patch.mjs 24/24:用官方同款 parseDocument + tag:yaml.org,2002:js 解析,再按加载器的方式 new Function('ctx','expr','with(ctx){return eval(expr)}') 求值;同时验证 state.json 缺失/损坏时不抛异常(否则 DSH 启动就会炸) |
门面在没有任何子进程时就能应答 initialize、tools/list 返回空数组、tools/call 明确报错 | scripts/smoke.mjs:这是整套架构的前提 |
门面把上游不合规的 inputSchema: {} 补齐成对象 schema,且修完的整份 tools/list 能通过真实 SDK 的 ListToolsResult 校验 | scripts/smoke-install.mjs 第 1 节:合成子进程里故意放一个零参数工具(auth/platforms,inputSchema: {}),断言 ① 门面输出 type:"object" ② 已合规的 schema 逐字节未被改写 ③ 同一时刻直连子进程拿到的仍是 {}(对照,证明是门面修的)④ 用 DSH 自带 @modelcontextprotocol/client 的 specTypeSchemas.ListToolsResult 校验通过 |
| 管理 API 的 Origin 校验、设置校验、端口漂移 | scripts/smoke.mjs |
| 校验失败后删除坏包(否则下次会复用半截文件)、找不到二进制时报错而非静默成功 | scripts/selfcheck-download.mjs 5/5 |
| 校验值不匹配必须拒绝,且不破坏现有安装 | scripts/smoke-install.mjs 第 4 节:清单里给真实 sha256,本地文件被改一个字节 → 拒绝,并在错误里同时给出期望值与实际值 |
| 无校验值默认拒绝、显式确认后放行(设置与单次覆盖两条路径) | scripts/smoke-install.mjs 第 5 节 |
zip 与 tar.gz 两条解压分支、单层顶层目录拍平、可执行位、config.yaml 落在安装根目录且 0600 | scripts/smoke-install.mjs 第 1–2 节 |
启动 → 健康检查 → 门面转发真实工具列表 → REST {code,data} 解包 | scripts/smoke-install.mjs 第 1 节 |
| 升级后回滚目标仍然可用、回滚后服务健康 | scripts/smoke-install.mjs 第 2–3 节 |
子进程被 SIGKILL 后自动拉起(换 pid、重启计数 +1) | scripts/smoke-install.mjs 第 7 节 |
| 设置界面里的五个平台登录:能力协商、凭据不落盘不进日志、授权门禁、华为免验证码分流 | scripts/check-platform-auth.mjs 54/54(真实 API handler + 假子进程,MCP 与"子进程连接被拒"两条路径都覆盖);真机探针(v1.2.19 + 本机已登录米家):能力协商只报 10 个授权工具、华为卡片自动缺席、米家授权地址真实生成、涂鸦假用户代码 400 原样透传、米家退出 501、 |
真实发行包上的端到端(不是合成载荷)
mac-arm64 v1.2.20,从 GitHub Releases 真下载 6,901,681 字节,本地实算 sha256 = 5fd06524…bd6b,与 manifest/server-manifest.json 中的值逐字符一致(该字节数与校验值已独立复核)。随后:解压出 5 个条目(config.yaml / lib/ / README.txt / webui/ / miloco-mcp-server,顶层没有目录,所以平铺是空操作)→ 去除 macOS 隔离属性 → 先签 lib/libmiot_camera_lite.dylib 再签主程序(顺序颠倒会把已签好的依赖覆盖成无效签名)→ mode 755 → file 报 Mach-O 64-bit executable arm64 → codesign --verify 主程序与 dylib 均 OK → 二次调用幂等返回。
src/download/* 另有 34 项行为用例与 14 项网络用例(自签 HTTPS 覆盖 302 跟跳、503 重试、404 不重试、重定向到 http 被拒、重定向上限、空闲超时、取消、ftp 拒绝),以及一次 11,219,258 字节的真实下载与清单校验。
仍未覆盖的:DSH profile 里真实加载一次(npm test 用假 Cordis 上下文),以及设置页在真实 GUI 中的渲染。
所有行为描述(端口、路径、状态机、字段归属、API 与门面、界面字段)都来自已落地的源码与三份确认过的设计文档。文档与实现冲突时以源码为准,并请顺手改这份文档。
已知不一致(需要维护者或用户拍板)
-
cordis.patch.yml 的注释与 maxAttempts: 60 口径矛盾 已修正:注释现在写明"默认 10 次 / 约 2.5 分钟"是本插件要抬高的默认值,60 次只是缓冲;门面的真正理由是"静态 YAML 端口拿不到运行期端口"。见 ADR-0001。
-
!!js 表达式读 state.json 的前提未实测 已实测:见 scripts/check-patch.mjs(24 项)。仍未在真实自启动流程里验证的是"入口行按补丁顺序初始化、且 apply() 被 await"——它由 vendor/loader/src/config/entry.ts 的 _start()/await fiber.await() 支撑,属源码级证据。不成立时的退化路径是"改门面端口需重启 DSH",即固定 38081。
⚠️ 改这个文件时最容易踩的坑:url: !!js "…" 是多行引号标量,收尾那一行也必须比 url: 缩进更深。收尾行与键同缩进时,解析器会在内容行就认为标量结束,整个 patch 解析失败 —— 而 dsh-app-boot 遇到解析错误是直接抛错,后果是 profile 起不来。scripts/check-patch.mjs 就是为这个坑写的,build.sh 会跑它。
-
dsh.client.inject 只列 @deepseek-ai/dsh-client-ui-slots,而 tsdown.config.ts 允许外部化的名单有 8 项(react、react/jsx-runtime、react-dom、react-dom/client、@deepseek-ai/cordis、-client-store、-client-ui-slots、-client-ui-primitives)。这是有意的不对称:外部化名单是"允许留成裸 require 的上限",inject 是"实际依赖的清单",而 client/index.tsx 刻意不引 primitives。如果界面以后开始引 primitives 或 store,必须同步加进 inject,否则前端加载会失败。
-
server.ws_port 无常量 已修正:新增 DEFAULT_WS_PORT(constants.ts),config-gen.ts 引用它。它不暴露在界面上 —— 插件自身不跑 WebSocket(MCP 桥走 Streamable HTTP),但子进程要求该字段存在。
-
huawei.device_id / device_name:子进程读取它们,但插件不生成(不在 MANAGED_FIELDS 里),源码里也没有对应的回写函数。这两个值的来源尚未核实。
-
多个 DSH 实例共享一个安装根(~/.dsh/dsh-feyagate:state.json / server.pid / versions/ / cache/ 各一份)。本机实测有三个实例跑同一个 profile,2026-09-25 18:16–18:20 期间后台服务被 杀了 8 次、每次退避重启 —— 每个实例启动时都看到"别人的子进程还没应答健康检查",于是,形成互杀。已修两处(见 ):① 给"活着但尚未应答"的进程 (),期间轮询并优先( 第 8 节);② :pid 文件记录 ,看门狗只重启(或属主已退出)的进程;别人的子进程卡死时只如实报告"属于另一个仍在运行的 DSH 实例",不再杀它( 10 项)。门面的代理目标也改为优先用它自己 supervisor 的端口,不再依赖机器级共享的 。:在任一实例点「停止服务」仍会停掉共享的那一个(这是显式操作);建议日常只保留 DSH 实例使用本插件。
License
MIT © 2026 FeyaGate Contributors
本许可证只覆盖本仓库的源码与文档,不覆盖运行时下载的 miloco-mcp-server 二进制,详见 NOTICE。
English summary
dsh-feyagate-gateway is an open-source (MIT) plugin for DeepSeek Harness that manages a local miloco-mcp-server background service and exposes its 76 MCP tools to the model as mcp__feyagate__*.
- Dual-half npm package + Cordis plugin. Host half (
exports["."]) owns download, checksum, install, supervision and upgrade. Browser half (exports["./client"]) renders one settings.section with Service / Accounts (read-only overview) / License tabs.
- Two rows in
cordis.patch.yml: the plugin itself, plus the stock @deepseek-ai/dsh-mcp-client bridge (serverName: feyagate). The bridge points at the plugin's always-on loopback MCP facade (default 127.0.0.1:38081, recorded in state.json and read by a !!js expression at boot), never directly at the child process. The child's real port (default 38080, drifts when occupied) therefore stays a runtime detail, and installing the service after boot does not burn the bridge's reconnect budget.
- Layout:
~/.dsh/dsh-feyagate/ with state.json, a generated config.yaml at the root (the child resolves every relative path, including data/, against the directory holding that file, and binds its device identity and license there — so the root never moves), child-owned data/, logs/server.log, immutable versions/<version>/, cache/, and a current pointer file.
- Integrity: authoritative
manifest/server-manifest.json (real upstream file names + sha256, because asset naming drifts between releases), sources degraded in order GitHub Releases → FOTA → configurable mirror → local package, always verify before extracting; assets without a checksum require explicit user opt-in.
- No binaries in this repository, ever. The Xiaomi proprietary
libmiot_camera_lite is never downloaded or redistributed; when upstream omits it, cameras are unavailable and the plugin reports a degraded state with a reason.
- Plugin self-upgrade reuses
dsh plugin update and requires a host restart; the MVP only detects a new version and shows a copyable command.