chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进

1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
This commit is contained in:
admin committed 2026-09-15 18:47:13 +08:00
1 parent c70d5d860e
commit 5ad755116e
173 files changed
+27632

No files matched your search

@@ -0,0 +1,222 @@
# VoxEMW 全云 API 化:接入 dsh 修订方案(零自托管模型)
- **日期**:2026-09-09(22:50 版)
- **性质**:对《VoxEMW接入dsh调研与落地方案_20260909.md》的**方向性修订**——用户决策:**模型全部连线上 API,不部署任何本地模型**
- **一句话**:不再"代理运行 VoxEMW 那套 GPU 服务",而是**以 VoxEMW 为产品与体验蓝本,复用其 persona 文案与 `assets/mojingnvwu/face_ref.jpg` 形象资产,后端全部换成 2026 年已商业化的云端实时 API**,由 dsh 插件做编排/代理/隔离。
> ⚠️ **重要更正**:前一版文档 §4 判断"实时写实数字人渲染没有等价公开 API"——该判断已被 2026-09 市场现状推翻:火山引擎「实时互动数字人 API(FlowAct-R1)」、阿里云「数字人实时交互 OpenAPI」、ZEGO「精品照片数字人」均已提供"图片/形象 + 音频流 → 实时视频流"的商业 API。全云 API 路线**可行**,本文即按此重写。
---
## 一、决策影响对照(原方案 → 修订方案)
| 维度 | 原方案(代理 VoxEMW GPU 服务) | 修订方案(全云 API) |
|---|---|---|
| 模型部署 | 4090 主机四模型本地常驻(21.7G/24G) | **零本地模型**,全部云端 API |
| GPU 主机/隧道 | 需要 | **移除**,不需要任何自管 GPU |
| VoxEMW 代码 | 运行上游 python 全套 | **不运行**,仅作蓝本(UI 氛围/协议形态/人设) |
| 核心工作 | 守护 + 反向代理 + 槽位 | **云端编排层**(Realtime WS 对接 + 数字人流对接 + 人设注入) |
| 用户数据隔离 | 槽位 + persona 映射 + 零持久化 | 槽位/配额 + profile 维度记账 + 云端 session 独立 + 第三方合规提示 |
| 需要决策的新增项 | — | 供应商组合、数字人形象图、音色路线(见 §七) |
---
## 二、2026-09 云端供给盘点(VoxEMW 六积木逐块替换)
### 2.1 替换矩阵
| VoxEMW 积木 | 等价云 API(已核实存在) | 关键参数/证据 |
|---|---|---|
| ① VAD + ② STT | 并入"端到端 Realtime 模型"(自带 server VAD + 自动打断),或单独接讯飞/火山/阿里实时 ASR | 端到端更省事,见 2.2 |
| ③ LLM 大脑 | 端到端模型自带大脑;或保留 DeepSeek API(分离式时) | 魔镜人设走 `instructions`(session.update),与 VoxEMW 注入 persona 同思路 |
| ④ TTS 音色 | 端到端预置音色(列表切换);要"专属音色/克隆"则分离式接 MiniMax / 阿里 CosyVoice / 火山豆包 | 见 §六体验差异(音色设计能力是最大降级点) |
| ⑤ **写实数字人** | **火山「实时互动数字人 API (FlowAct-R1)」**:单张人物图 + 16kHz PCM 音频流 → 实时视频流,480P@25fps,首帧 ~2s,声画毫秒同步([docs.volcengine.com](https://docs.volcengine.com/docs/86081/2387261))<br>或 **阿里云数字人实时交互 OpenAPI**:WebSocket + streamed audio driver,支持 `customUserId`(天然适合租户标记)([help.aliyun.com](https://help.aliyun.com/en/me/getting-started/digital-human-real-time-interactive-openapi))<br>或 **ZEGO 精品照片数字人**:1 张照片 200ms、1080P,走 RTC 视频互动([doc-zh.zego.im](https://doc-zh.zego.im/aiagent-mini-program/introduction/overview)) | 单图输入 → **`assets/mojingnvwu/face_ref.jpg` 可直接复用** |
| ⑥ 眼睛 VLM | **GLM-Realtime**(音视频通话模型,WebSocket,支持摄像头帧输入、function calling、server VAD、可打断)<br>或 **阿里 Qwen-Omni-Realtime / qwen3.5-omni-plus-realtime**(DashScope,WS 与 WebRTC 双通道,视频帧输入) | 摄像头帧走同一条 Realtime WS,天然实现"她看得见你" |
### 2.2 端到端 Realtime vs 分离式(架构大方向二选一)
| | 端到端 Realtime(**推荐**) | 分离式(ASR + LLM + TTS 各选一家) |
|---|---|---|
| 做法 | 一个 WebSocket 完成"听→想→说",服务端 VAD/打断全托管 | 每环节独立 API,自行拼装状态机 |
| 延迟/体验 | 低(0.3~0.5s 级开口),打断顺滑 | 每跳多一次网络,拼装复杂、易抖 |
| 音色自由度 | 预置音色列表(GLM-Realtime 提供 tongtong/xiaochen/female-tianmei…) | 可用 MiniMax/CosyVoice 克隆"魔镜专属嗓音" |
| 视觉(眼睛) | GLM-Realtime / Qwen-Omni 直接吃视频帧 | 需另接 VLM API |
| 计费参考 | GLM-Realtime-Flash:音频 0.18 元/分,视频 1.2 元/分;Air:0.3 / 2.1 元/分([docs.bigmodel.cn](https://docs.bigmodel.cn/cn/guide/models/sound-and-video/glm-realtime)) | 各家按量,总价通常更高 |
**推荐组合**(两个候选,M1 前拍板):
- **组合甲(默认推荐)**:端到端大脑+声音 = **智谱 GLM-Realtime**;出画 = **火山 FlowAct-R1**(单图+音频流)。理由:中文生态、GLM-Realtime 带视频+function calling、成本低、火山数字人与豆包端到端语音同族可平滑替换。
- **组合乙(同厂商偏好)**:端到端 = **阿里 Qwen-Omni-Realtime**(视频帧原生);出画 = **阿里实时数字人 OpenAPI**(带 `customUserId`,租户标记友好)。理由:一家计费/控制台,WebRTC 浏览器直连低延迟。
---
## 三、目标架构(修订后)
```
┌──────────── 用户浏览器(dsh 会话窗口右侧分栏面板,同源 https)───────────┐
│ 魔镜面板(轻量前端,蓝本=VoxEMW web/ 的氛围,但重写为云版) │
│ ├─ 麦克风采集 16kHz PCM → host 代理 → 云 Realtime WS │
│ ├─ 摄像头帧(可选"眼睛")→ host 代理 → 同一 Realtime WS │
│ └─ 云数字人视频流 → <video> 播放(火山 FlowAct-R1 / 阿里) │
└──────────────────────┬──────────────────────────────────────────────────┘
│ 同源(ws/wss + http),Authorization 由服务端注入
┌──────────────────────▼── dsh 服务器(47.77.182.89 / dsh 域)────────────┐
│ dsh-plugin-voxemw-cloud │
│ ├─ lib/host.js:/voxemw/api/* + /voxemw/ws/* 反向代理到云厂商 │
│ │ · 云 API Key 保管于此,**绝不下发浏览器** │
│ │ · 注入 dsh 登录态 → 按 profile 换取一次性云端会话 token │
│ ├─ lib/slot.js:会话槽位仲裁 + 配额(按 profile 限每日用量) │
│ ├─ lib/billing.js:profile × 分钟数 × 费用的用量记账(审计) │
│ └─ lib/client.js:会话分栏 + 魔镜面板 UI(沿用 mcn split 技术) │
└─────────────────────────────────────────────────────────────────────────┘
│ HTTPS/WSS(云厂商公网 API)
▼
智谱 GLM-Realtime / 阿里 Omni-Realtime(大脑+耳朵+嗓子+眼睛)
火山 FlowAct-R1 / 阿里实时数字人(出画,输入 face_ref.jpg 形象)
```
要点:
1. **云 API Key 全部收口在 dsh 服务端**,浏览器永远只连 dsh 同源地址 → 无 CORS、无密钥泄露、天然 secure context。
2. 数字人形象输入 = 复用 VoxEMW 仓库 `assets/mojingnvwu/face_ref.jpg`(280K 单张正面像,符合 FlowAct-R1"清晰正面半身图"要求)。
3. 魔镜人设 = 复用 `personas/mojingnvwu.md` 正文,经 `session.update.instructions` 注入(与 VoxEMW 注入 s2s 同一思路);音色从云端预置列表近似选(见 §六)。
4. VoxEMW 上游 python **不再被运行**;前端也需**重写轻量云版**(原 `web/` 深度耦合 orchestrator 协议/本地假设,不能直接指向云)。
---
## 四、dsh 插件形态(包结构修订)
> **命名统一(2026-09-09 实现时裁定)**:插件名定为 **`dsh-plugin-voxemw-cloud`**(v0.1.0),替代早期方案的 `dsh-plugin-voxemw` 提法,旧称不再使用。
> **M1 交付边界**:客户端 bundle 只能在运行中的 dsh 实例加载验证(红线:client 改动必须重启实例 + 打包缓存),本机开发仅做语法/逻辑冒烟测试;实例级验证步骤见 README。云端厂商链路需账号开通后接线,realtime/avatar 先以"协议适配层 + 纯函数"落地(可测),UI 侧仿 social-workbench 提供"未配置云端"引导态。
```
dsh-plugin-voxemw-cloud/
├─ package.json / cordis.patch.yml # 三段式骨架,注册 __mcnEntries,feature-tier
├─ lib/
│ ├─ index.js # 路由注册:控制面 /voxemw/api/* + 面板页 /voxemw/app + /voxemw/health
│ ├─ slot.js # 槽位 + 配额(云会话按分钟计费 → 必配每日上限)
│ ├─ billing.js # profile 维度用量/费用记账(审计表)
│ ├─ cloudcfg.js # 厂商/Key/模型/音色/形象 配置(服务端保管,密钥不下发)
│ ├─ realtime.js # Realtime 协议适配层:session.update(人设/音色) 构建等纯函数
│ ├─ avatar.js # 数字人协议适配层:会话初始化请求构建等纯函数
│ └─ client.js # bundle:工作台入口 + 面板(沿用 mcn 分栏宿主,iframe 同源)
├─ web/app.html # 魔镜面板轻量前端(单文件,同源加载,含麦克风回环自测)
└─ README.md
```
---
## 五、用户数据隔离(修订)
| 数据 | 存放/流向 | 隔离措施 |
|---|---|---|
| 用户语音/摄像头帧 | 浏览器 → dsh 代理 → **云厂商** | 每次会话新建独立云端 session;代理不留音频副本(纯透传);关闭面板即销毁 |
| 对话/转写 | 云端会话内存(GLM 音频通话上下文 ~8K/20 轮) | 会话结束即释放;需留存时按 profile 落用户目录,绝不跨用户复用 |
| 人设/热词 | dsh 服务端按 profile 存映射 | 注入仅限本人会话 |
| 计费/用量 | billing 表(profile 维度) | 每用户只见自己的记账 |
| 云 API Key | dsh 服务端环境变量 | 浏览器不可见;轮换/最小权限(RAM/子账号 key) |
**新增合规注意(P1)**:语音/画面会经第三方云厂商处理——产品上需对用户明示;自用/内网不受影响。厂商选型时优先国内合规厂商(智谱/阿里/火山均支持企业实名)。
**沿用上一版 §6.2 的槽位层**:端到端模型与数字人流都按会话/分钟计费且厂商有并发限制(如 GLM Realtime 免费/低等级并发 5 路),slot 互斥 + 每日配额仍然是刚需。
---
## 六、体验差异(诚实对照,避免上线后落差)
| 体验点 | 原版(本地 4090) | 云 API 版 | 影响 |
|---|---|---|---|
| 开口延迟 | 说完 ~3s(本地 s2s 链路) | 端到端云 Realtime 通常相当或更低(server VAD 判停即响应) | ✅ 不降级 |
| 出画首帧 | SoulX 常活待机,几乎即时 | 火山 FlowAct-R1 首帧 ~2s;ZEGO 200ms 但走 RTC | ⚠️ 需预热/占位动画掩盖,或选 ZEGO |
| 音色"设计感" | VoxCPM2 描述词凭空造嗓 + 种子钉定 | 端到端只有预置音色;专属嗓音需分离式克隆(需参考音频) | ⚠️ **最大降级点**,见决策 3 |
| 魔镜形象 | SoulX 写实渲染 | 云端数字人(face_ref.jpg 驱动或平台形象) | 观感不同但可接受 |
| 长会话稳定性 | 本地单时钟唇形同步 | 厂商提示"过长视频有崩坏概率,建议会话长度策略"(火山) | 需设单会话时长上限并自动续段 |
| 运营成本 | GPU 租用(AutoDL 时按小时) | 按分钟计费(参考:音频 0.18~0.3 元/分 + 视频 1.2~2.1 元/分,纯语音组合更省) | 低频个人使用成本低;高频需配额 |
---
## 七、需要你拍板的 3 个决策(M1 前置)
1. **供应商组合**:组合甲(智谱 GLM-Realtime + 火山 FlowAct-R1,默认推荐)还是组合乙(全阿里)?还是只要纯语音(先不做出画,最省事)?
2. **数字人形象**:直接用 VoxEMW 自带 `assets/mojingnvwu/face_ref.jpg`,还是换一张更符合"魔镜女巫"设定的形象图(含版权确认)?
3. **音色路线**:先用云端预置女声(接近即可,最快)→ 之后若需"魔镜专属嗓音"再接分离式 TTS 克隆(需你提供一段 3~10s 参考音频,或用描述词在支持语音设计的 TTS 上逼近原 seed 效果)?
---
## 八、实施路线(修订)
**M1 — 云端链路跑通 + 面板可见(半天~1 天)**
1. 注册厂商账号、开通 API(按决策 1);服务端保管 key。
2. 独立验证页:麦克风 → Realtime WS 对话成功;音频流喂数字人 API → 视频出画。
3. 接入 dsh:host 代理 + 会话分栏面板内嵌云版前端。
- ✅ 验收:dsh 会话旁打开魔镜,可语音对话 + 数字人出画;关闭面板会话无损。
**M2 — 隔离/配额/记账/人设**
4. slot + 每日配额;profile 维度 billing;人设注入;face_ref.jpg 形象固化;占用/排队 UI。
- ✅ 验收:双账号并发互斥、各自人设/记账不可见;用量超限自动拒 claim。
**M3 — 体验与合规**
5. 音色定制(决策 3 后半);长会话自动续段/上限;窄栏 UI 打磨;日志分级;第三方数据处理提示。
---
## 九、访问形态、服务器负载与并发容量(2026-09-09 增补,回应"是否还是插件/压力多大/支持多少人")
### 9.1 它仍是 dsh 插件,访问入口不变
全云 API 化**没有改变"插件"形态**——改变的是插件内部"不装模型、只做编排/代理"。访问链路分两个角色:
| 角色 | 是什么 | 访问方式 |
|---|---|---|
| **使用方(dsh 用户)** | 登录 dsh 后,在会话窗口分栏点「魔镜女巫」入口 | 插件 client bundle 注入 `__mcnEntries`,点击展开右侧面板 → 浏览器采集音视频、渲染数字人,全程**不感知云厂商存在** |
| **dsh 平台自身** | dsh 服务端运行 `dsh-plugin-voxemw-cloud`(host 半区) | 持有各家云凭证;控制面调用厂商 API 换取会话;把短时效凭证交给浏览器;按 profile 做槽位/配额/记账;也可向 dsh agent 暴露 MCP 工具("启动/切换人设/查占用") |
> 所以"dsh 如何访问"的答案 = **浏览器访问 dsh 同源入口,dsh 插件进程访问云厂商**,中间没有其他系统。
### 9.2 两条数据面架构(决定服务器压力,M1 前必须拍板)
| | 路径 | 服务器压力 | 适用 |
|---|---|---|---|
| **A. 媒体面直连(推荐)** | 浏览器 ↔ 云厂商 RTC/WS 直连音视频;dsh 服务器只做**控制面**(登录态校验 → 向厂商换短时效会话凭证 → 下发浏览器 → 记账) | **极小**:每个活跃会话仅几十 KB/s 级信令/文本,无媒体转发 | 云厂商支持浏览器直连 + 临时凭证。已核实线索:Qwen-Omni-Realtime 明确支持 WebRTC 浏览器低延迟;ZEGO/火山走 RTC 房间模型天然直连;GLM-Realtime 为 WS+API Key,直连会暴露 key,需厂商临时凭证或走 B |
| **B. 服务端中转(WS 全代理)** | 浏览器 → dsh 插件代理 → 云厂商 | 媒体全部过服务器:**带宽=瓶颈**(见 9.3 量化) | 厂商只有 WS+长期 Key(如 GLM-Realtime 默认);或无浏览器 SDK |
**建议**:M1 验证时逐厂商问清"浏览器直连 + 临时凭证(ephemeral token/RTC room 凭证)"支持度,优先 A;A 不可用的环节退回 B 并控制并发。
### 9.3 服务器负载量化(估算,供规划)
单会话媒体流量(最坏=厂商给原始 PCM,若走 RTC/Opus 会小 4~8 倍):
| 流 | 方向 | 码率估算 | 说明 |
|---|---|---|---|
| 上行语音 | 用户→云 | 16kHz PCM16 ≈ **256 kbps**(Opus 则 ~24–32 kbps) | 说话时才满速 |
| 下行语音 | 云→用户 | GLM 输出 pcm24 ≈ **384 kbps** | — |
| 数字人视频 | 云→用户 | 480P@25fps 估 **0.8–1.5 Mbps**(厂商未公开,按同类流媒体估) | 仅出画档 |
- **架构 A(直连)**:以上流量全部不经 dsh 服务器 → **dsh 服务器负载≈0**,只剩登录校验/凭证下发/账单(每会话可忽略)。dsh 服务器**不是瓶颈**。
- **架构 B(中转)**:每路活跃全功能会话 ≈ 上行 256k + 下行 1.7M ≈ **~2 Mbps**(不出画纯语音约 0.6 Mbps)。按带宽估并发:10 Mbps 出口 ≈ 5 路全功能(或 ~16 路纯语音);1 Gbps ≈ 500+ 路(理论,另受云配额/CPU 转发限制)。**即:若要中转且大规模,带宽决定上限。**
### 9.4 支持多少人同时访问(分层容量模型)
"同时访问"要拆成三层,瓶颈各不相同:
| 层 | 含义 | 瓶颈 | 规模预估(推荐架构 A) |
|---|---|---|---|
| ① 同时在线 | 登录 dsh、面板能打开(不对话) | dsh 服务器 + 云账号配额外的静态资源 | 数百~上千不成问题,服务器压力≈0 |
| ② 同时语音对话 | 占用一条云端 Realtime 会话 | **云厂商并发配额**(例:GLM-Realtime 低等级在途并发 5 路起,可付费升级)| 通常买 5~50 路;受成本约束 |
| ③ 同时数字人出画 | 占用一路云数字人渲染 | 云数字人按路/分钟计费的并发上限 | 单账号通常个位数~十路级,需与厂商确认 |
**结论**:
- 瓶颈**不在 dsh 服务器**(只要走媒体面直连 A);瓶颈在**云厂商并发配额**和**按分钟费用**。
- 推荐按"**槽位数 = 你买的云端路数**"来卖/分配:例如买 5 路 → 同时最多 5 人占用魔镜,第 6 人排队(slot + 配额机制正是为此设计)。这也是为什么 §四 slot/billing 是刚需。
- 若坚持服务端全中转(B),则按 9.3 公式用你 dsh 服务器实际出口带宽反推上限。
> ⚠️ 各厂商具体并发配额/直连凭证机制随套餐变化,**M1 开通账号后实测**(一次开 N 路压测),本表为规划级估算。
---
## 附录:信息来源
- 火山引擎「实时互动数字人 API (FlowAct-R1)」:https://docs.volcengine.com/docs/86081/2387261
- 阿里云「数字人实时交互 OpenAPI」:https://help.aliyun.com/en/me/getting-started/digital-human-real-time-interactive-openapi
- ZEGO「实时互动 AI Agent 2.0 / 精品照片数字人」:https://doc-zh.zego.im/aiagent-mini-program/introduction/overview
- 智谱「GLM-Realtime」:https://docs.bigmodel.cn/cn/guide/models/sound-and-video/glm-realtime
- 阿里「Qwen-Omni-Realtime / qwen3.5-omni」:https://help.aliyun.com/en/model-studio/realtime
- VoxEMW 复用资产:`D:\tmp\voxemw\voxemw-src\assets\mojingnvwu\face_ref.jpg`、`personas/mojingnvwu.md`(MIT 许可,复用保留版权声明)
@@ -0,0 +1,353 @@
# VoxEMW 接入 dsh:全面调研与落地方案
> ⚠️ **2026-09-09 22:50 方向修订**:用户已决策「模型全部连线上 API、不部署本地模型」。本版(代理 VoxEMW GPU 原套)**已被《VoxEMW全云API化接入dsh修订方案_20260909.md》取代**为本方案主路径;本文件保留作"本地/云端 GPU 跑 VoxEMW 原套"的备选存档。VoxEMW 上游架构事实(§二)与 plugin_package 分栏技术(§三)在两版间通用。
- **日期**:2026-09-09
- **调研对象**:github.com/emwstudio/VoxEMW(git tag v1.10.0,源码已本地快照 `D:\tmp\voxemw\voxemw-src\`)
- **参考实现**:`D:\dshworkspace\plugin_package\`(dsh-plugin-mcn / dsh-plugin-social-workbench / dsh-plugin-douyin-accounts)
- **目标**:① 能否以插件形式加入 dsh;② 使用时在「会话窗口旁边」显示;③ 用户相关数据隔离
- **结论**:**可行,但不是把 VoxEMW 代码"搬进"插件,而是做一个 dsh 侧 host/feature 插件去"守护 + 同源代理 + 内嵌"VoxEMW 这个独立的数字人服务**,会话区采用 dsh-plugin-mcn 已验证的「会话窗口 split + 拖拽分栏」渲染。隔离通过「单槽互斥会话 + profile 维度的 persona/状态映射 + 零持久化语音策略」实现。
---
## 一、结论速览(TL;DR)
| # | 问题 | 结论 | 关键依据 |
|---|---|---|---|
| 1 | VoxEMW 是什么 | 一套**单机 4090 满血写实数字人**(魔镜女巫):浏览器语音对讲 + 数字人视频 + VLM 视觉,LLM 大脑走 DeepSeek API | README / `docs/plan-4090.md`,四模型同卡 21.7G/24G |
| 2 | 能否"以插件形式加入 dsh" | ✅ 可以,但**插件不含 VoxEMW 本体**。VoxEMW 是 GPU 重型、多进程、带独立 Python 虚拟环境的服务,只能作为**被插件管理的独立服务**存在 | 服务形态:orchestrator(:8000)+s2s(:8765)+SoulX(:8791)+VLM(:18099) |
| 3 | 形态参照 | = **dsh-plugin-social-workbench 的"守护+内嵌 iframe"** + **dsh-plugin-mcn 的"会话分栏"** 二者叠加 | plugin_package 已实证两套技术 |
| 4 | 会话窗口旁边显示 | ✅ 采用 mcn 已验证方案:把 `[data-slot='conversation']` 包进 flex 容器,点入口后在会话右侧展开面板(可拖 5px resizer),VoxEMW 前端以 iframe 同源加载 | mcn `client.js` 的 `.mcnNav_split` CSS 与 `startResize` |
| 5 | 用户数据隔离 | ✅ 分三层实现:**槽位互斥**(orchestrator 只支持单会话,二次连接会顶掉前者——必须加 claim 管理器)+ **profile 维度映射**(persona/配置按用户隔离,VoxEMW 本身无多租户)+ **零持久化**(VoxEMW 会话状态全在内存,天然无跨用户残留) | orchestrator.py `current_session` 顶替逻辑;Session 类无磁盘状态 |
| 6 | 主要风险 | GPU 前置(需一张 4090 或等效);**多用户并发被物理限制为 1 路**;VoxEMW 前端需要 secure context(https/wss)才给麦克风/摄像头权限;无鉴权需由 dsh 侧补 | 见 §6 风险清单(P1/P2) |
| 7 | 总体投入 | M1 服务接入 + 内嵌可见(核心)+ M2 槽位/隔离/人设映射 + M3 加固 | 见 §7 路线 |
---
## 二、VoxEMW 项目解剖(决定一切的事实基础)
### 2.1 形态判断
- 名字/形态:**魔镜女巫数字人**,v1.10.0 主线 = "4090 满血写实数字人版"。Mac/VRM 轻量档在 tag v1.9.0(不同形态,本次不讨论)。
- 典型部署:一张 AutoDL 4090(24GB),**四模型同卡常驻**:Qwen3-ASR-1.7B(STT)+ VoxCPM2(TTS)+ SoulX-FlashHead-1.3B(写实数字人)+ MiniCPM-V-4.6(视觉),显存 ~21.7G/24G。
- 大脑:**DeepSeek API**(`deepseek-v4-flash`),不是本地模型。所以"本地 4090 + 云上大脑"。
- 一句话:**这是端到端实时语音/视觉/数字人应用,重 GPU、多进程、自带头部追踪调度(pacer)**,与"浏览器端插件"是两种物种。
### 2.2 运行拓扑与边界(端口/进程/环境)
| 积木 | 端口/协议 | 进程/环境 | 职责 | 代码位置 |
|---|---|---|---|---|
| orchestrator | :8000 aiohttp HTTP+WS | `py312` 主 venv | 浏览器唯一入口:会话调度 / persona 注入 / 打断编排 / RTC 信令 / 静态服务 | `voxemw/gateway/orchestrator.py`(840 行) |
| s2s 语音管线 | :8765 realtime WS | 同 venv(`pipeline.launch`) | VAD→STT→LLM→TTS 全链路,`num_pipelines: 1` | `voxemw/pipeline/*` |
| SoulX 渲染 | :8791 WS | 独立 `flashhead` venv | 音频驱动写实 talking-head | `voxemw/avatar/soulx_server.py` |
| VLM 边车 | :18099 | 独立 `vlm` venv | MiniCPM-V 看图/OCR,供 `look_at_camera` 工具 | `voxemw/gateway/vlm_server.py` |
| 前端 | —— | 纯静态 | 画布/麦克风/摄像头/WebAudio/WS/RTC | `web/index.html`(48行)+`assistant.js`(909行)+`style.css` |
启动脚本:`scripts/start_4090.sh`(全启/stop);配置:`configs/assistant-4090.yaml`(4090 档,`host: 0.0.0.0`)与 `configs/assistant.yaml`(Mac 档,只绑回环)。
### 2.3 前端页面硬依赖(决定 iframe 方案的约束)
`web/index.html` + `assistant.js` 是一个**全屏沉浸式单页**(canvas 星空背景 + 魔法镜 + 麦克风圆钮 + 实时转写区),运行时需要:
1. **getUserMedia 麦克风**(上行语音)、**摄像头**(仅 `look_at_camera` 触发时抓帧 POST `/vision/frame`)。
2. **WebSocket** `/ws`(上行音频/控制,下行转写/状态/音频 delta)+ 可选 **WebRTC**(`/rtc/offer`、`/rtc/ice`,Mac/LAN 档开、4090 远程档关,走 WS+WebAudio)。
3. **Secure Context 硬前提**:浏览器只在 https(或 localhost)下授予 getUserMedia 与 WebAudio。这意味着 **VoxEMW 页面必须经 https 域名对外**(直连裸 IP 的 http://ip:8000 拿不到麦克风)。
4. 若以 iframe 内嵌,iframe 需 `allow="microphone; camera"`,且页面本身必须处于安全上下文。
### 2.4 浏览器侧协议契约(引自 orchestrator.py 顶部 docstring)
```
/ws 文本帧(JSON):
→ {"type": "vox.persona", "id": "<persona_id>"} 切换人设(可运行时切换)
→ OpenAI Realtime 事件原样透传(input_audio_buffer.append / response.cancel …)
← OpenAI Realtime 事件透传(transcription / response.done …)
← {"type": "vox.status", "persona": "<id>", "rtc": {"enabled": bool, "ice_servers": [...]}}
GET /api/personas → 人设清单(默认人设 + 列表)
POST /rtc/offer → WebRTC 信令(body {"sdp","type"} → answer)
GET /rtc/ice → ICE 配置
POST /vision/frame → 视觉帧上报(VLM 用)
/static/* → 前端静态资源
```
### 2.5 单会话硬约束(本方案最核心的工程约束)
`orchestrator.py` 明确写着 **"单用户单会话:新浏览器连接顶掉旧会话"**:
```python
current_session: dict = {"session": None} # 全局只有一个槽
...
async def ws_handler(request):
old = current_session["session"] # 顶掉旧的
...
current_session["session"] = session # 新连接独占
```
深层原因:s2s 只有 1 个管线槽(`num_pipelines: 1`)+ 一张 4090 的渲染算力只够一路写实数字人。**这是上游物理/架构决定,不是配置能改的**。任何"多用户并发各自开一路魔镜"的设想都必须先接受这个天花板。
推论:多用户接入必须走**槽位仲裁(slot claim)**:同一时刻只有 1 个 dsh 用户能占用魔镜,其余用户看到"占用中"并可排队/订阅释放。
### 2.6 状态持久化现状(对"用户数据隔离"是重大利好)
- Session 状态**全部在内存**:`_assistant_history` 仅保留近 2 轮回复(回声判定用),随连接销毁即清。
- **无用户语音/转写落盘**(默认不开 live transcription;`enable_live_transcription: false`)。
- 唯一"持久"的是 `personas/*.md`(人设正文 + 音色描述/种子),由配置文件静态装载,启动时进 `config.personas.resolved` 字典。
- 结论:VoxEMW 侧几乎没有可跨用户残留的数据。**"用户数据隔离"主要矛盾在 dsh 侧的接入编排层,而非 VoxEMW 数据层。**
### 2.7 安全现状(需 dsh 侧补齐)
- orchestrator **无鉴权、无多租户、无审计**(4090 档直接 `host: 0.0.0.0`)。若裸奔公网:任何人可白嫖你的 DeepSeek key 和 GPU。
- LLM key 走进程级环境变量 `DEEPSEEK_API_KEY`,单实例单 key,**无法按用户区分计费/配额**。
---
## 三、参考实现拆解:plugin_package 给了哪三块模板
### 3.1 dsh 客户端插件的"三段式骨架"(三个包一致)
```
dsh-plugin-xxx/
├─ package.json # main=lib/index.js; exports{ "." , "./client" };
│ # dsh.client.platform="web" (+可选 inject:["@deepseek-ai/dsh-client-*"])
│ # dsh.bundle.patch="./cordis.patch.yml"
├─ cordis.patch.yml # 声明式注册本插件 bundle 层:- insert: [{id, name}]
├─ lib/index.js # 服务端半区(在 dsh 进程内):注册 HTTP 路由 / MCP 工具描述、守护外部服务
└─ lib/client.js # 客户端 bundle:window.__ModuleLoader__.load({id, factory}),
# 内部 exports.apply()/exports.inject[](Cordis 应用层注入)
```
`cordis.patch.yml` 示例(三个包一致,mcn-nav / social-workbench / douyin-accounts 各一条 insert)。
### 3.2 模板 A:social-workbench —— "守护 + 内嵌独立服务"(与 VoxEMW 场景最同构)
- 场景:Easel 是**独立的**社媒工作台(自带 .venv + web 前端,默认 :7860)。
- host 半区(`lib/index.js`)只做守护/探活,不搬 Easel 代码:
- `GET /swb/status`(探活 web/gateway、安装状态、子进程存活、日志尾)
- `POST /swb/start` / `POST /swb/stop`(拉起/停掉独立进程)
- `GET /swb/log`(返回日志尾)
- 环境变量覆盖:`EASEL_DIR`、`EASEL_WEB_PORT`、`EASEL_SWB_AUTOSTART`
- client 半区(`lib/client.js`):`apply()` 里注册 `window.__mcnEntries.push({id:'social-workbench', icon, title, component})`,component 渲染一个含 `iframe src=state.webUrl` 的面板(状态条 + 启动按钮 + 内嵌页)。
- **可复制点**:VoxEMW 插件 host 半区 = `/voxemw/status|start|stop|log` + 代理;client = `__mcnEntries` 注册 + iframe 内嵌。
### 3.3 模板 B:dsh-plugin-mcn —— "会话窗口旁边显示"的标准答案
mcn 的 client bundle 实现了**把聊天窗口与工作台面板左右分栏**的技术,全部在浏览器 DOM 层完成,代码证据(`lib/client.js`):
```css
.mcnNav_split{display:flex;flex-direction:row;flex:1 1 auto;min-width:0;min-height:0;
width:100%;height:100%;overflow:hidden;...}
.mcnNav_split>[data-slot='conversation']{display:contents} /* 会话被包进容器 */
.mcnNav_split>[data-slot='conversation']>div{
flex:1 1 0%!important; min-width:0!important; height:100%!important;
--dsh-chat-content-width:100%!important} /* 面板收起时满宽 */
.mcnNav_split>.mcnNav_resizer{flex:0 0 5px;height:100%} /* 拖拽条 */
.mcnNav_split>.mcnNav_panel{flex:0 0 auto;height:100%} /* 右侧面板 */
```
- `startResize()`:mousedown 记录 startX/startW → mousemove 改面板宽度 → 面板侧记忆 `localStorage["mcnNav.panelSide"]`。
- 机制本质:**把 dsh 的 `[data-slot='conversation']` DOM 节点移入插件自己的 flex 容器**,展开时塞入 resizer+panel,收起时等价恢复满宽(`display:contents` + `--dsh-chat-content-width` 兼容聊天区自身宽度变量)。
- 生态分工:**mcn 是"壳"**(注入左侧导航 + 提供 `window.__mcnEntries` 注册表 + 会话分栏),**其余功能插件是"页"**(douyin-accounts / douyin-video-detail / mcn-schedule / social-workbench 全部通过 `window.__mcnEntries.push/unshift` 注册,点击后在右侧面板渲染页面)。
> 因此"在会话窗口旁边显示"在 plugin_package 生态里的标准做法 = **做一个 feature-tier 插件,注册进 `__mcnEntries`**;前提是宿主 dsh 已安装 mcn 壳(用户环境天然满足:plugin_package 就是这套生态)。
### 3.4 模板 C:douyin-accounts —— host 路由 + 页面 CRUD 的 API 形态
- `lib/index.js` 暴露 `/mcn/api/*`(accounts CRUD、列表、分析等)HTTP 路由;同时也以工具描述形式暴露给 agent(MCP/工具语义),前端直接 fetch 调用。
- **可复制点**:VoxEMW 的"会话控制/人设/状态"控制面做成 `/voxemw/api/*`,页面与 agent 双通道可调。
---
## 四、可行性判定与形态选择
| 形态 | 做法 | 判定 | 原因 |
|---|---|---|---|
| 形态 0:二开/搬代码 | 把 voxemw python + 4 个模型装进 dsh 进程/客户端 bundle | ❌ 不成立 | ① GPU 重负载 + 独立 venv 多进程,无法进客户端 bundle;② 违反既有红线(不改写官方 bundle、插件不打包重型服务);③ 跨平台(Windows 开发机无 GPU/模型装不上) |
| 形态 1:**host/feature 插件 + 同源代理 + iframe 内嵌**(推荐) | VoxEMW 部署在 GPU 主机(本地或远程),dsh 侧插件守护它,并通过 dsh 所在 https 域同源反向代理 `/voxemw/*`;客户端在会话分栏 iframe 打开 | ✅ **推荐** | social-workbench 同构已验证;同源代理一举解决 secure context(麦克风/摄像头权限)与 ws 透传;插件本体轻量、可独立升级;VoxEMW 仍可独立更新 |
| 形态 2:iframe 直连 GPU 主机裸址 | `iframe src=http://gpu-host:8000` | ⚠️ 不推荐 | 裸 http 非安全上下文 → 麦克风/摄像头被浏览器拒绝;跨源 ws 需要 VoxEMW 侧放行 CORS/Origin(上游未做);token 无法注入 |
**最终形态(形态 1)一句话**:`dsh-plugin-voxemw` = social-workbench 的守护骨架 + mcn 的会话分栏 + VoxEMW 自己的 web/ 页面做面板内容,页面流量经 dsh 域名同源代理到 GPU 主机 orchestrator。
---
## 五、总体架构设计
### 5.1 目标拓扑
```
┌────────────────────────────── dsh Web (https://dsh.alotbuy.com 或同级域) ──┐
│ dsh 主进程 │
│ └─ dsh-plugin-voxemw (feature-tier, 注册 __mcnEntries) │
│ ├─ lib/index.js host 半区 │
│ │ ├─ /voxemw/api/status|start|stop|slot|personas 控制面 │
│ │ ├─ /voxemw/proxy/* 同源反代 → GPU 主机 orchestrator │
│ │ │ (HTTP /ws /static /rtc/offer /vision/frame 全透传) │
│ │ └─ SlotManager(进程内): 槽位互斥 + profile 维度状态 │
│ └─ lib/client.js client bundle │
│ ├─ 会话分栏 (mcn 同款: [data-slot=conversation] 包 flex) │
│ └─ 面板: iframe allow="microphone; camera" src=/voxemw/ │
│ + 状态条(占用/空闲/排队) + 弹出全屏 │
└──────────────┬──────────────────────────────────────────────────────────┘
│ 仅插件 host 出网(隧道 / 白名单 443 / 内网)
▼
┌────────────── GPU 主机(AutoDL 4090 或自有 GPU 机)──────────────────────┐
│ scripts/start_4090.sh │
│ orchestrator :8000 ← s2s :8765 · SoulX :8791 · VLM :18099 │
│ 全部只绑回环/私网,由 dsh 侧代理访问;DEEPSEEK_API_KEY 在此进程内 │
└─────────────────────────────────────────────────────────────────────────┘
```
### 5.2 dsh-plugin-voxemw 包结构(交付蓝图)
```
dsh-plugin-voxemw/
├─ package.json # 三段式骨架;name=dsh-plugin-voxemw
├─ cordis.patch.yml # insert: [{id: voxemw-nav, name: dsh-plugin-voxemw}]
├─ lib/
│ ├─ index.js # host:SlotManager + /voxemw/api/* + /voxemw/proxy/*
│ ├─ slot.js # 槽位仲裁:claim/release/queue/keepalive(profile 维度)
│ ├─ personas.js # 用户↔人设 映射(每 profile 一份 persona 清单,可热同步到 VoxEMW personas 目录)
│ ├─ proxy.js # 同源反向代理(http/ws 透传、超时、断线自愈、按 profile 注入会话 token 查询参数)
│ ├─ supervisor.js # VoxEMW 进程/远程隧道 守护与探活(对标 social-workbench /swb/*)
│ ├─ client.js # bundle:__ModuleLoader__.load + 会话分栏 + 面板 iframe + 状态 UI
│ └─ (可选)embed/ # 对 VoxEMW web/ 的极简适配(窄栏 CSS 注入、标题/关闭、全屏弹层)
├─ build.mjs # client 打包(esbuild/rollup → 单文件 bundle,css 内联)
└─ README.md
```
### 5.3 关键流程时序
**① 打开魔镜(claim + persona 注入)**
```
用户点击侧栏「魔镜女巫」→ mcn 会话分栏展开右侧面板 → iframe 载入 /voxemw/(同源,安全上下文)
→ client 调 POST /voxemw/api/slot/claim {profile}
→ SlotManager: 空闲? 分配 claimId(30min 续期) : 返回 occupied(+排队序号)
→ 面板显示「占用中 / 空闲 / 排队中」
→ iframe 内 VoxEMW 前端建立 /ws;插件在 URL 上注入 ?claim=claimId&persona=<该profile映射人设>
→ VoxEMW 前端 connect 后发送 vox.persona(或由代理改写首个消息)完成人设落定
```
**② 会话中**
- 语音全走 VoxEMW 自己链路(浏览器→代理→orchestrator→s2s/模型),dsh 只做透传,不落语音。
- 面板关闭/页面卸载 → `POST /voxemw/api/slot/release`;异常断线由 supervisor keepalive 超时回收(orchestrator 本身也会被新连接顶掉,双保险)。
**③ 占用冲突(核心场景)**
- 用户 B 打开 → claim 被拒 → 面板给「魔镜正被 用户A 使用」+ 订阅(slot 释放事件推送,B 可一键接管)。
- 可选策略(配置项):B 请求「强制接管」→ 插件先优雅 release A(A 端收到 `vox.status{evicted:true}` 提示)。
### 5.4 代理路由表(/voxemw/proxy/* 透传清单)
| 上游 | 方法 | 用途 |
|---|---|---|
| `/`、`/static/*` | GET | VoxEMW 前端静态页(走代理 = 同源 = 安全上下文) |
| `/ws` | WS 升级 | 实时语音/控制主通道 |
| `/api/personas` | GET | 人设清单(按当前 profile 过滤后返回) |
| `/rtc/offer`、`/rtc/ice` | POST/GET | RTC 信令(4090 档默认关闭,保留透传能力) |
| `/vision/frame` | POST | 视觉帧上报 |
> 若 GPU 主机与 dsh 不在一台:优先 **SSH 反向隧道**(`ssh -R` 或 autossh 保活,只把 orchestrator :8000 映射到 dsh 主机的 127.0.0.1:18000),代理指向 `http://127.0.0.1:18000`,彻底避免 GPU 主机暴露任何公网端口;GPU 主机防火墙仅放行 dsh 主机 IP 的出向建立。
---
## 六、用户数据隔离设计(多租户)
### 6.1 数据面与威胁面
| 数据类别 | 存在哪 | 生命周期 | 跨用户风险 |
|---|---|---|---|
| 用户语音流 | 浏览器内存 → WS → VoxEMW s2s(内存) | 会话结束即清 | 无(不落盘) |
| 摄像头帧 | 浏览器内存 → `/vision/frame`(`_last_frame` 单帧覆盖) | 单帧 | 无 |
| 转写/回复文本 | orchestrator 内存 + 浏览器转写区 | 会话结束即清 | 无 |
| persona/音色 | `personas/*.md`(VoxEMW 侧,共享目录) | 持久 | **中**:若共享目录,A 建的人设 B 可切到 |
| 会话占用状态 | SlotManager(dsh 进程内) | 实时 | 需要防串号 |
| LLM 调用(DeepSeek) | 云 | 账单 | **中**:单 key,无法按用户区分成本 |
| dsh 插件日志 | dsh 日志文件 | 持久 | 低:可能含转写/人设,需分级 |
### 6.2 三层隔离策略
1. **槽位层(并发互斥)**:SlotManager 保证同时只有 1 个 claim 存活;claim 的 key = `profileId`,**任何接管/释放都校验 claimId 归属**,杜绝 A 顶掉 B 或读到 B 的会话(VoxEMW 的"新连接顶旧连接"特性被槽位仲裁封死在上游)。
2. **配置层(persona/人设)**:VoxEMW 的 personas 由启动配置装载。落地:
- 每 profile 的 persona 包(`{profile}/voxemw/personas/*.md`)与 orchestrator 的共享 personas 目录**分离管理**;
- 插件持有"profile → persona_id"映射表(存 dsh 侧 profile 维度,不存 VoxEMW);
- 变更时走"热切换":会话内发 `vox.persona`;新增 persona 需重启 orchestrator 装载的场景,由 supervisor 在**无 claim 的空窗期**原子重载(`/voxemw/api/personas/reload`,M2/M3 范围)。
3. **数据/密钥层**:
- 语音/帧/转写**默认零持久化**(维持上游 `enable_live_transcription:false`),需要留存时显式开启并按 profile 落 `$DSH_HOME/storages/` 下用户目录,权限沿用 dsh setpriv/account 边界;
- `DEEPSEEK_API_KEY` 保持进程级,dsh 侧**不接触该密钥**;成本控制靠槽位互斥 + claim 期配额(每 profile 每日时长上限,超限拒 claim)+ 日志记账(profile 维度调用统计);
- 插件自身状态(claim/映射/审计)全部以 `profileId` 为主键,输出到 profile 自己的工作区。
### 6.3 与 dsh 既有隔离模型的衔接
- 若 dsh 部署为**每 profile 独立实例**(`ISOLATION=account` + setpriv):插件 index.js 天然运行在用户实例内,SlotManager 需改为**跨实例协调**(共享一个小型注册表:Redis/文件锁/Unix socket,按 profileId 加锁);推荐把 SlotManager 做成 dsh 主进程侧单例,实例侧只做 claim RPC。
- 若 dsh 为**集中式多用户**(dshs 按 token 分离会话):插件在请求上下文拿 `profileId`,所有写操作按 profile 命名空间隔离,逻辑同上。
### 6.4 风险清单(P1/P2)
| 级别 | 风险 | 说明 | 缓解 |
|---|---|---|---|
| P1 | 多用户并发被物理限制为 1 路 | 上游单管线单会话 + 单卡算力 | 槽位互斥 + 排队 + 强制接管策略(产品上明示"魔镜单席位") |
| P1 | 无鉴权裸奔 | orchestrator 0.0.0.0 无认证,公网可白嫖 | 永不直连公网;仅 dsh 代理访问;GPU 主机 443/白名单;代理层对 `/voxemw/*` 校验 dsh 会话 |
| P1 | 前端需安全上下文 | 裸 http 拿不到麦克风/摄像头 | 一律经 https 域同源代理;iframe `allow="microphone; camera"` |
| P2 | 共享 personas 目录串扰 | A 建人设 B 可见 | profile 维度 persona 包 + 映射表隔离(§6.2-2) |
| P2 | 单 DeepSeek key 无分账 | 无法按用户限流 | claim 配额 + profile 维度用量记账(M3) |
| P2 | 转写/文本进日志 | 共享日志跨用户可读 | 日志分级:语音链路事件降到 warning;含人设/转写日志按 profile 独立文件 |
| P2 | iframe 内嵌 UI 适配 | 全屏沉浸页塞窄栏体验打折 | embed/ 窄栏 CSS + 面板内全屏/新窗口模式;VoxEMW web/ 基本是响应式 canvas,适配成本低 |
| P2 | 远程隧道保活 | 断隧 = 面板失效 | autossh/心跳 + supervisor 探活;VoxEMW 自身有断线自愈,主要保链路 |
---
## 七、实施路线(里程碑 + 验证点)
**M1 — 服务可达 + 面板可见(1 个工作日量级)**
1. GPU 主机起 `start_4090.sh`,确认 orchestrator :8000 各积木就绪(`scripts/avatar_probe.py` / `s2s_text_probe.py` 可参考)。
2. dsh 侧先手工验证:BT 反代或 SSH 隧道把 8000 映射到 dsh 域 `/voxemw/`;浏览器开 https 页,确认麦克风授权、能对话、数字人出画。
3. 搭插件骨架(package.json + cordis.patch.yml + 空 apply),client 只做"会话分栏 + iframe 指向 /voxemw/",host 只做纯透传。
- ✅ 验证:在 dsh 会话旁看到魔镜面板且完整可用;关面板后会话区无损恢复。
**M2 — 槽位仲裁 + 隔离 + 人设映射**
4. SlotManager claim/release/keepalive + 占用/排队 UI + 强制接管。
5. profile→persona 映射 + 会话内自动 `vox.persona` 注入;personas 目录 profile 化改造。
6. 代理层注入会话校验(非 dsh 登录态一律 401);面板关闭自动 release。
- ✅ 验证:双账号交替/并发用例(A 占用时 B 看到占用并可排队/接管;A 会话不被 B 看到);A 的自定义人设 B 不可见。
**M3 — 加固与运营**
7. 日志分级/脱敏(按 profile 分文件);用量记账 + 配额;supervisor 状态页(对标 /swb/status:隧道健康、各积木 up/down、claim 占用者)。
8. 窄栏 UI 适配 polish + 全屏/新窗口模式;`README` 安装运维文档。
- ✅ 验证:审计日志只含本人数据;GPU 主机零公网端口可被外部探测;断隧自动恢复。
---
## 八、环境前置与成本(不回避)
| 项 | 要求 | 备注 |
|---|---|---|
| GPU | **1× 4090 24GB(或同档)**,四模型 ~21.7G | 无 GPU 则只有 Mac 轻量档(v1.9.0,无写实数字人)可选,架构同但产物不同 |
| 网络 | dsh 主机 ↔ GPU 主机可建隧道/白名单;dsh 域名 https | 复用现网 dsh.alotbuy.com 反代能力 |
| 密钥 | `DEEPSEEK_API_KEY`(GPU 主机环境变量) | dsh 侧不持有 |
| 模型下载 | HF:Qwen3-ASR / VoxCPM2 / SoulX-FlashHead / MiniCPM-V | 按 `docs/plan-4090.md` 预装(一次性) |
| dsh 侧 | 已装 mcn 壳(plugin_package 生态) | 本插件走 feature-tier 注册 |
| 兼容性红线 | 不改写 `/usr/local/lib/node_modules/@deepseek-ai/dsh` 官方 bundle;不动 VoxEMW 上游 | 全部改动隔离在 dsh-plugin-voxemw 目录 + GPU 主机私有部署目录 |
---
## 附录 A:代码证据索引(供复核)
| 结论 | 证据 |
|---|---|
| VoxEMW 六积木架构 | `README.md`("架构(六块积木)")、`docs/plan-4090.md` |
| 路由表 | `voxemw/gateway/orchestrator.py` L779-787(add_get/add_post/add_static) |
| 单用户单会话 | `orchestrator.py` L678 `current_session: dict`;L733-765 `ws_handler` 顶替逻辑;顶部 docstring "单用户单会话" |
| 浏览器协议 | `orchestrator.py` docstring(/ws 帧、/rtc/offer、/rtc/ice) |
| 无状态 | `Session._assistant_history` 仅 2 轮(L225);`enable_live_transcription:false` |
| personas 静态装载 | `assistant.yaml` personas.list;orchestrator L640 `config["personas"]["resolved"]` |
| 守护+内嵌模板 | `dsh-plugin-social-workbench/lib/index.js` L4-7(/swb/status\|start\|stop\|log);`client.js` iframe L115、`__mcnEntries` apply() |
| 会话分栏技术 | `dsh-plugin-mcn/lib/client.js`:`.mcnNav_split` CSS、`[data-slot='conversation']{display:contents}`、`.mcnNav_resizer{flex:0 0 5px}`、`startResize`、`localStorage["mcnNav.panelSide"]` |
| feature-tier 注册 | `douyin-accounts/lib/client.js` L649-652 `window.__mcnEntries.push({id:'douyin-accounts',...})` |
## 附录 B:名词对照
| 名词 | 说明 |
|---|---|
| s2s | speech-to-speech 实时语音管线(VAD→STT→LLM→TTS) |
| pacer / audio_pacer | orchestrator 内音频滴灌调度(对齐数字人渲染) |
| persona | 人设包:正文 + 音色(VoxCPM2 voice_control/voice_seed) |
| __mcnEntries | mcn 壳暴露给功能插件的页面注册表(window 全局) |
| data-slot='conversation' | dsh 聊天区 DOM 标记,插件以此定位并包入分栏容器 |
| claim / SlotManager | 本方案引入的"魔镜单席位"占用仲裁 |
@@ -0,0 +1,205 @@
# 13-插件管理面(修订 v5:入库审查门禁 + 用户自选启用,Step 0 核查结论已回填)
> 状态:**方案草案 v5,待用户拍板 3 个决策点后实施**|2026-09-10|对象:dshs + dsh 0.1.2-rc.1
> v3 范围(07:57):普通用户可查看 admin 上传的全部插件,卡片多选 →「启用」弹窗确认 → 才复制进用户 profile → 重启该用户 dsh 实例后生效。
> v4 范围(08:00):admin 上传一律先进审查区,过「安全检测 + 多用户检测」无 P0/P1 才可发布入库;用户选择页仅展示 published。
> v5 范围(08:1x,Step 0 服务器实测回填):**平台已自带部分插件机制**(见 §四),本方案收敛为在其上新增四件事——admin 全局库+审查门禁、用户拉取式安装(复制进 profile)、启用后**带新 patch 立即重启**(新增 relaunch)、插件选择页。启用状态源修正为 DB `folder_plugins`(非 profile patch disabled 行)。
> 评审稿:`D:\AI技能\aliyun-dsh-server\插件管理面_调整方案草案_20260910.md`;核对后定稿 `04-调整方案/` 档案 13 双端同步。
## 一句话结论
平台已能"列出用户 profile 已装插件 + 按工作区 folder 勾选启停(launch 时渲染 patch 生效)",且有桌面端「插件」面板(desktop.html);**缺的是上游**:admin 的全局插件库与入库审查门禁、用户把库插件**安装**进自己 profile 的入口、启用后**立即生效**(现有机制只在下次 launch 生效)。本方案补上这四块:admin 上传 → 审查区(安全 S1-S10 + 多用户 M1-M9,P0/P1 阻断,报告留痕)→ 发布入库;用户在「插件选择页」浏览 published 库 → 卡片勾选(批量)→「启用」弹窗确认 → 后端 安装(复制进 profile + 追加 bundles;已装则跳过)→ 写入当前工作区 enabled 集 → **relaunch(重算 patch 并重启实例)** → 生效。
## 一、角色与流程
| 操作 | admin | 普通用户 |
|---|---|---|
| 查看已发布插件库(选择页) | ✅ | ✅ |
| 上传插件(进审查区,≠入库) | ✅ | ❌ |
| 检测/出报告(安全 + 多用户) | ✅(自动 + 复核终审) | ❌ |
| 发布入库 / 打回需改造 | ✅ | ❌ |
| **安装**(从库复制进自己 profile) | ✅ | ✅(选择页,随启用自动) |
| **启用**(= enabled 集 + relaunch) | ✅ | ✅ **核心动作** |
| **禁用**(= 移出 enabled 集 + relaunch) | ✅ | ✅ |
| 删除/移除已发布包(全局) | ✅ | ❌ |
**插件入库时序(admin)**:同 v4(upload → under_review → 自动检测 S/M → 无 P0/P1 + admin 复核 → release → published)。
**用户启用时序(v5 精确流程,对齐平台真实机制)**:
```
1. 打开「插件选择」页 → GET /api/plugins/library?folder=<当前工作区>
→ published 全量 × 本人状态:未安装 / 已安装-未启用 / 已启用(本 folder)
2. 勾选卡片(可批量)→ 点「启用」
3. 弹窗确认:插件清单 + 「将安装到你的个人配置并重启你的 dsh 实例,当前会话可能中断」
4. POST /api/plugins/library/enable {names:[...], folder}
5. 后端 per 插件:
a. 未安装 → 从 published 库复制包目录 → profile/node_modules/<pkg>
+ chown 用户 uid/gid(先顶层再子项)+ profile package.json dsh.profile.bundles 追加
(冲突预检:短 id/路由/官方名/同名不同版本 → 409)
b. enabled 集(DB folder_plugins,按 folder workspace)写入该插件 id
c. orchestrator.relaunch(user):按该 folder 重算 patch = renderPatch(enabled∩installed)
→ 以新 patch 重启 main + watchdog(复用既有 launch 的计算逻辑,抽公共函数)
6. 返回 → 页面状态刷新 = 已启用;工作台出现插件入口
```
**禁用时序**:勾选已启用插件 → 「禁用」→ 确认 → enabled 集移除该 id → relaunch → 入口消失(profile 副本保留,再次启用免复制秒级恢复)。
## 二、总体机制
```
admin 上传 ─▶ 审查区 under_review(dataRoot/plugin-reviews,用户不可见,root 600)
│ 自动静态检测 S1-S10 + M1-M9 → 报告(P0/P1/P2)
├─ P0/P1 → needs_fix 需改造 ─▶ 改造重传复检
└─ 无 P0/P1 + admin 复核 ─▶ release ─▶ published 库
│ dataRoot/plugin-library(root 600 只读)
▼
插件选择页 plugins.html(仅 published)
│ enable = 安装(复制进 profile)+enabled集+relaunch
▼
用户 userRoot=dataRoot/users/<uuid>/
├─ home/profiles/web/package.json ← dsh.profile.bundles(installed 源)
├─ home/profiles/web/node_modules/<pkg> ← 复制落点(chown 用户)
├─ home/profiles/web/cordis.patch.yml ← role patch,勿动(ensure-role-profile-patch.cjs 管)
└─ patches/main.yml ← launch/relaunch 渲染的 --patch(DB enabled 派生)
▼ relaunch(新 patch)→ dsh --profile web 重启 → 生效
```
- **installed 与 enabled 是两态**:installed = profile 已含该 bundle(复制+声明);enabled = 当前工作区 folder 的 DB enabled 集(决定 launch patch 是否注入)。桌面端既有「插件」面板只做 enabled 勾选(下次 launch 生效,不重启);选择页做"库浏览 + 安装 + 启用(立即 relaunch)"。
- **共享库 = 源,profile = 副本,enabled 集 = 状态**;禁用 ≠ 卸载(副本保留)。
- 用户选择页仅 published;审查区/报告仅 admin 可见。
## 三、入库审查门禁(安全检测 + 多用户检测)— 同 v4
**放行规则**:canRelease = 无 P0 且无 P1;P2 不阻断留档;admin 人工复核为 release 必选终审;检测只读扫描、不执行包内脚本;报告 JSON 留痕(root 600,按版本保留)。
**安全检测 S1-S10**:
| # | 检查内容 | 级别 | 整改建议 |
|---|---|---|---|
| S1 | 归档:恰一个顶层目录;无 `..`/符号链接逃逸/设备文件 | **P0** | 重打包 |
| S2 | package.json 合法;`dsh.bundle.patch` 存在且指向存在文件 | **P0** | 补清单 |
| S3 | `@deepseek-ai/*`、官方同名 | **P0** | 改名(红线 2) |
| S4 | install/postinstall 等 npm 脚本 | **P0** | 移除 |
| S5 | exec/spawn、动态 eval/Function、vm 逃逸 | P1 | 说明+复核;纯函数化 |
| S6 | http(s) 出网非白名单 | P1 | 登记域名+用途 |
| S7 | 混淆/隐藏文件/超大二进制(>5MB 说明) | P1 | 去混淆可审计 |
| S8 | fs 写绝对路径/越出插件目录/触碰他人 profile | **P0** | 改注入的相对路径 |
| S9 | chown/root 级写/监听 0.0.0.0 | P1 | 移除,编排器统一管 |
| S10 | client 硬编码 token/密钥/内网地址 | P1 | 走 profile 密文配置 |
**多用户检测 M1-M9**(针对单用户 dsh 开发假设):
| # | 检查内容 | 级别 | 整改建议 |
|---|---|---|---|
| M1 | 硬编码绝对路径/固定配置位置 | P1 | 注入的相对路径/dataDir |
| M2 | 独占固定端口/0.0.0.0 | P1 | 实例内路由/动态端口 |
| M3 | 共享外部服务单槽(VoxEMW current_session 教训) | P1 | 无状态/云 API/多槽 |
| M4 | 持久化写到 profile 外共享全局文件 | **P0** | profile 内 JSONL |
| M5 | 短 id/路由/`__mcnEntries` 键与已发布或审查中冲突 | **P0** | 登记表比对改唯一 |
| M6 | 模块级可变单例跨租户串用 | P1 | 随实例或按用户键 |
| M7 | 无锁写共享资源/假定唯一活跃会话 | P1 | 单写锁/编排器串行 |
| M8 | 大模型/GPU/长驻进程等资源声明 | P2 | 卡片标注,admin 视容量放行 |
| M9 | 假定热加载/无需重启 | P2 | 明示需重启(页面已含提示) |
## 四、Open Questions 前置核查结论(Step 0 服务器实测,2026-09-10)
| # | 结论(实测事实) | 对实现的影响 |
|---|---|---|
| OQ1 | **profile 布局 ✅**:dataRoot=`/var/lib/dshs`(env `DSHS_DATA_ROOT`);`userRoot=dataRoot/users/<uuid>`(owner=每用户 uid/gid,如 `dsh-eeccbc...`/100002);profile=`userRoot/home/profiles/web`(MAIN_PROFILE `web`);installed 源=profile `package.json` 的 `dsh.profile.bundles`;包实体=`profile/node_modules/<pkg>` | 复制落点=该 profile node_modules;写后 chown 用户 uid/gid(先顶层再子项) |
| OQ2 | **复制式安装未实证,待 dry-run**:现 profile node_modules 无 `@deepseek-ai/*` 实体(未运行/由 dsh CLI 首启安装);listInstalledPlugins 按 `node_modules/<pkg>/package.json` 读 description | 实现后用无害测试插件在测试用户上 dry-run:复制 → listInstalledPlugins 可见 → relaunch 生效;若 pnpm/安装逻辑清掉手放目录 → fallback `dsh plugin add`(走 CLI) |
| OQ3 | 新用户 profile 由 spawn 首启 `dsh --profile web` 创建(09-09 建号已见 profiles/web + node_modules 目录骨架) | 复制目标不存在 → ensureDir + 最小骨架 package.json(bundles:[])+ chown;极端情况走 OQ1 错误引导 |
| OQ4 | **✅ 有用户域重启端点但语义修正**:`POST /api/dsh/restart {command}`(requireAuth,操作 `request.user.id`)调用 `orchestrator.restartMain`,但 **restartMain 复用内存里的旧 patch,只能重启不能换插件集** | **新增 `orchestrator.relaunch(userId)`**:按当前 main 的 folder 从 DB 重算 patch(与 launch route 同逻辑,抽公共函数)→ 以新 patch 重启 main + watchdog。enable/disable 内部调用它,不新增公开端点亦可 |
| OQ5 | **✅ 平台已有插件启停面(桌面端)**:`desktop.html`「插件」面板调 `GET /api/plugins?folder=`(已装 + 每 folder enabled)与 `POST /api/plugins/select`(存 DB `folder_plugins`),**无重启动作,下次 launch 生效**;backend 已注册 `pluginRoutes`(09-08 底座)。官方 dsh 会话内设置是否另有插件分区:无运行实例未实证,UI 验证阶段补查 | 双 UI 定位:选择页 = 库浏览+安装+启用(立即 relaunch);桌面面板 = 已装插件快速勾选(保留)。desktop 加提示"装新插件请到插件选择页";若官方会话设置确有启停分区,再加互链(记录为验证项,不阻塞 v1) |
| OQ6 | **✅ 状态源修正**:installed = profile `dsh.profile.bundles`(listInstalledPlugins 现有实现);enabled per folder = DB `folder_plugins`(workspaces 按 user+folder 键)。**草案旧假设"profile cordis.patch.yml 写 enabled/disabled 行"不适用**;且 profile 现存 cordis.patch.yml 是 role patch(`ui-settings-models` disabled,ensure-role-profile-patch.cjs 管理)——**勿手改** | enabled 一律走 DB 集操作(getOrCreateWorkspace + setFolderPlugins / getEnabledPluginIds 已存在) |
| OQ7 | 冲突检测:复制时按 name 比对 profile 已装版本(bundles + node_modules 包 version);库 release 时同名同版本拒绝覆盖 | 409 version-conflict;卡片显示"已装 vX,库 vY" |
| 附加 | launch route 已实现 patch 计算(`renderPatch(enabled∩installed)`)但为内联逻辑;`renderPatch` 恒注入 `dshs-runtime`(supervisor/patch.ts) | 抽 `patchForLaunch(...)` 公共函数,launch/relaunch 共用,行为零变化 |
## 五、API 设计
统一基址 `/api/plugins`;鉴权 requireAuth / requireAdmin。**既有 `/api/plugins` GET 与 `/api/plugins/select` 保留不动**(桌面端在用)。
**用户(新)**
| 接口 | 行为 |
|---|---|
| `GET /api/plugins/library?folder=` | published 全量(name/version/desc/hasClient/hasServer/reviewedAt/pkgSize)+ 本人状态 `{state: none\|installed\|enabled, installedVersion}` |
| `POST /api/plugins/library/install` | body `{names:[], folder?}` → 仅安装(复制+chown+bundles 追加),不启停 → `{ok, installed:[...]}`(供"仅安装稍后启用") |
| `POST /api/plugins/library/enable` | body `{names:[], folder?}` → 未装则先安装 → 写 enabled 集 → **relaunch** → `{ok, enabled:[...], restarted:true}` |
| `POST /api/plugins/library/disable` | body `{names:[], folder?}` → 移除 enabled 集 → relaunch → `{ok, disabled:[...], restarted:true}` |
**Admin(新)**
| 接口 | 行为 |
|---|---|
| `POST /api/plugins/admin/reviews` | 上传进审查区 → 自动检测 → `{reviewId,name,version,status,report}` |
| `GET /api/plugins/admin/reviews` | 审查区+库全条目(status/版本/报告摘要/P0·P1 计数/历史) |
| `GET /api/plugins/admin/reviews/:id` | 详情 + 最新报告全文 |
| `POST /api/plugins/admin/reviews/:id/release` | 发布入库;有 P0/P1 → 409 has_blockers;同名同版本 → 409 version-conflict |
| `POST /api/plugins/admin/reviews/:id/reject` | 打回 needs_fix + `{comment}` |
| `DELETE /api/plugins/admin/reviews/:id` | 丢弃审查条目 |
| `DELETE /api/plugins/admin/library/:name` | 移除已发布包(用户副本不受影响) |
| `GET /api/plugins/admin/users` | 只读矩阵:每用户 installed/enabled 状态 + 版本 |
错误码:`400 invalid_plugin_name / unsupported archive type / archive must contain exactly one top-level dir / missing package.json(dsh.bundle) / reserved-package(@deepseek-ai/*) / archive member escapes root`、`404 not_found`、`409 already_enabled / has_blockers / version-conflict / id-conflict`。
上传校验规则同 v4(恰一顶层目录、dsh.bundle.patch 必在、防穿越、禁 @deepseek-ai/*;校验通过 ≠ 入库)。
## 六、改动文件清单(服务器 /opt/dshs,备份 `*.bak-YYYYMMDD-HHMM`)
| 文件 | 改动 |
|---|---|
| `src/config.ts` | 加 `pluginLibraryDir`(默认 `<dataRoot>/plugin-library`)、`pluginReviewDir`(`<dataRoot>/plugin-reviews`) |
| `src/fs/plugins.ts` | 扩展:`installToProfile`(复制+chown+追加 bundles)、`isInstalled`/`installedVersion`(OQ7 比对) |
| `src/fs/plugin-library.ts` | **新增**:published 库读写/列表/删除(root 600 语义) |
| `src/lib/pluginScan.ts` | **新增**:静态检测引擎(S1-S10+M1-M9 + 分级报告,只读不执行) |
| `src/supervisor/orchestrator.ts` | **新增 `relaunch(userId)`**:重算 patch + 带新 patch 重启 main + watchdog;launch 内 patch 计算抽公共函数(行为零变化) |
| `src/web/routes/archive.ts` | **新增**:解压+穿越校验+chown 公共 helper(skills/plugins/reviews 复用) |
| `src/web/routes/pluginLibrary.ts` | **新增**:用户库(list/install/enable/disable → 内部 relaunch) |
| `src/web/routes/pluginReviews.ts` | **新增**:admin 审查生命周期 + 库 CRUD + users 矩阵 |
| `src/web/server.ts` | register 新增路由 |
| `web/plugins.html` | **新增**:用户=published 库卡片多选+启用/禁用+确认弹窗(含"将重启实例");admin=审查区(状态/报告/发布/打回)+库管理+矩阵 |
| `web/desktop.html` | 加「插件选择」入口 + 面板内提示"装新插件到插件选择页" |
| `web/login.html` / `admin.html` | 入口(admin 视图) |
| `lib/**` | `npm run build`(tsc) |
| 文档 | 本档案 + `03-路线图与待办.md` 登记;双端同步 |
## 七、关键机制与约束
1. **installed/enabled 两态分离**:install=复制+chown+bundles;enable=enabled 集(DB)+ relaunch;disable=移除 enabled 集 + relaunch(副本保留)。
2. **relaunch 语义**:只重算 patch(enabled∩installed ∩ 已装过滤同 launch route)并带新 patch 重启,不落库不改 profile 配置;复用既有 spawn/waitForLaunchToken 流程。
3. **chown 纪律**:复制后 chown 用户 uid/gid(先顶层再子项,档案 11 教训);共享库/审查区 root 600;检测全程只读。
4. **确认弹窗必含**:插件清单 + "将安装到你的个人配置并重启你的 dsh 实例,当前会话可能中断"。
5. **并发写锁**:按用户 id 单写锁(install/enable/disable 串行化);launch 与 relaunch 天然互斥(mains 检查)。
6. **重启中状态**:relaunch 期间页面置"重启中",防"未重启仍见旧 bundle"误判(poc v0.4.3)。
7. **红线补充**:**不写/不改 profile 既有 `cordis.patch.yml`(role patch,ensure-role-profile-patch.cjs 管理)**;不动官方 bundle 与 dsh 主程序缓存;不自动升级 dsh。
## 八、验证清单
**API(curl)**
- [ ] 无 sid → 401;用户访问 admin 接口 → 403
- [ ] admin 上传 → under_review;用户 GET library 不含该包
- [ ] 恶意包:`..` 成员/@deepseek-ai/install 脚本 → P0 拒收;exec 动态串 → P1 → release 409 has_blockers;needs_fix 报告可查
- [ ] 多用户包:硬编码绝对路径(M1)/写 profile 外共享文件(M4)→ 命中打回
- [ ] 改造重传 v+1 → 复检 → release → published → 用户 library 出现(reviewedAt)
- [ ] 同名同版本 release → 409 version-conflict
- [ ] enable 单/多:安装副本入 profile(chown 正确)+ bundles 追加 + enabled 集写入 + relaunch;`restarted:true`
- [ ] 未装包 enable = 自动先 install;重复 enable → 409 already_enabled;disable → 集移除 + relaunch;再 enable 免复制
- [ ] OQ2 dry-run:复制式安装后 listInstalledPlugins 可见、relaunch 后入口生效;不符 → 切 CLI fallback 并记录
- [ ] 报告落盘 pluginReviewDir root 600;删除已发布包后已装用户不受影响
- [ ] 回归:`GET /api/plugins` 与 `/api/plugins/select`(桌面在用)行为不变;profile cordis.patch.yml 未被改写
**UI(浏览器,admin + 用户)**
- [ ] 用户:选择页仅 published 卡片(未安装/已安装/已启用角标)→ 多选 2 → 启用 → 弹窗文案正确 → relaunch → 工作台两插件入口出现(bundle rev 对比)
- [ ] 禁用 → relaunch → 入口消失;桌面「插件」面板勾选仍可用(下次 launch 生效)
- [ ] admin:上传 → 审查状态流转 → 报告 → 发布 → 用户端可见;矩阵正确
- [ ] 回归:技能管理面(档案 11)不受影响;官方 @deepseek-ai/* 任何视图不出现
## 九、风险
| 级别 | 风险 | 缓解 |
|---|---|---|
| P1 | relaunch 打断当前会话(单活跃实例) | 弹窗强提示 + 确认;relaunch 复用既有 spawn 流程,失败自动回退(watchdog 机制兜底) |
| P1 | 复制式安装与 profile 的 pnpm/dsh 安装逻辑冲突(手放目录被清/不可解析) | OQ2 dry-run 先行;不符即切 `dsh plugin add` CLI fallback |
| P1 | 静态检测为启发式,无法证明绝对安全/绝对多用户 | 人工复核必选终审 + 来源明确 + 报告留痕;沙箱试运行列为增强 |
| P2 | 与桌面既有插件面板/官方会话设置双 UI | 定位分工 + 页面互链提示;验证项补查官方会话设置 |
| P2 | 用户装多个大插件占 profile 空间 | 库卡片展示体积;副本体积纳入 admin 矩阵 |
| P2 | 审查放行依赖人工复核效率 | 自动覆盖硬规则(S1-S4/S8、M4/M5),人工只复核启发式项 |
## 十、红线遵守
不自动升级 dsh;不改官方主程序与缓存(只操作 plugin-library/plugin-reviews + 用户 profile 层);拒绝 `@deepseek-ai/*` 入库;上传包不执行任何脚本;**不改 profile 既有 cordis.patch.yml(role patch)与 ensure-role-profile-patch.cjs 产物**;共享库/审查区 root 600。