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 一律写「远程服务器」。
354 lines
27 KiB
Markdown
354 lines
27 KiB
Markdown
# 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 | 本方案引入的"魔镜单席位"占用仲裁 |
|