Files
dsh_ai1net_server/docs/外部接入/VoxEMW接入dsh调研与落地方案_20260909.md
T
admin ce8e6ceed9 chore(工作区): 纳入版本控制基线(回收 411 MB 过程产物)
回收 411 MB(470 M → 58.8 M),全部经回收站,可恢复:
- 待清理/(146.2 M,含 relay 分片 128 M 与 42 项过程目录)
- tmp/(32.4 M,按接续棒命名的过程临时区)
- .workbuddy/tmp/(39.5 M)
- 4 份 workbuddy.db 冗余副本(101 M,09-23 事故的坏副本 / 抢救产物)
- tmp/im16/gw/centrifugo 二进制(63.9 M,可重下)+ 缓存残留

入库范围:常驻规则(CODEBUDDY.md / README.md / state.py)、在途接续入口与
接续包、docs/、交付物/、交接单/、归档/、scripts/、.codebuddy/、
.workbuddy/memory/;共 398 件,其中 >60 KB 的 26 件全为文档。

排除(.gitignore):tmp/、待清理/、运行态日志与缓存、*.db 与 DB 备份整目录、
打包二进制(*.tar.gz / *.tgz)、记忆修复前备份。
2026-09-24 07:51:03 +08:00

354 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 | 本方案引入的"魔镜单席位"占用仲裁 |