Dw-LiveWorkbench
Zero-dependency AI workbench for livestream content: self-built RAG (BM25 + hashed n-gram vectors, recall@5 0.974), hand-written ReAct agent with 7 tools, multi-tenant RBAC, 372 automated assertions. 零依赖 AI 直播工作台 · RAG + Agent + 多租户
- Stars
- 0
- Language
- JavaScript
- Created
- Oct 19, 2026
- Updated
- Oct 24, 2026
Introduction
普洱茶直播工作台
面向普洱茶直播的本地工作台:从选题到 PPT、口播稿、提词、合规、复盘,一条线跑完。 零依赖安装,不需要联网,所有数据都是可直接查看和手改的 JSON。
一屏看完(技术亮点)
零第三方依赖(dependencies: {})· 372 条自动冒烟断言 · RAG 与 Agent 全部自研
| 能力 | 实测数字 |
|---|---|
| RAG 检索 | 字符 n-gram 哈希向量 + BM25 双通道,加权 RRF(rrfK=5,BM25 0.9 / 向量 0.1);552 个检索块;recall@5 = 0.974、MRR = 0.762(56 题标注集) |
| Agent 编排 | 手写 ReAct 循环(8 步 / 12 次工具调用硬上限);7 个工具(5 读 + 2 写);幻觉工具名 / 坏 JSON / 工具异常均回喂模型自纠正 |
| 写操作安全 | 两阶段提交:模型只能提议,人工确认后才执行;actionId = 工具名 + 规范化参数的 sha256 |
| 多租户 | AsyncLocalStorage 上下文(不穿参)、token 认证、三角色 RBAC、审计可追责、配额与成本计量 |
| 生成性能 | 方案生成 38 ms;批量 3 个山头 228 ms(确定性模板引擎,不调模型、零 token) |
| 工程规模 | 67 个 REST 接口;372 条冒烟断言;零依赖 |
复现所有指标
node scripts/rag-eval.mjs --offline # recall@5 / MRR(确定性,可逐字节复现)
node scripts/rag-probe.mjs # 零词表重叠探针 —— 专门暴露检索短板
node scripts/rag-smoke.mjs # 检索与拒答(28 条)
node scripts/agent-smoke.mjs # Agent 工具与写安全(147 条)
node scripts/smoke.mjs # 六大基础模块
已知限制(诚实)
- 没有规模化验证:真实用户就是作者本人,多租户只是骨架,没有第二家企业实际使用
- 向量不是神经嵌入:字符 n-gram 哈希,语义泛化弱;零重叠探针 8 条里 2 条灾难级跑偏
- 单机 JSON 存储:数据量小是优点(可读可手改),并发写有风险
- 没有性能压测:没有 QPS 数据,单进程
- Agent 未用框架(手写 390 行):可控、权限可自研;代价是没有 tracing / 多模型路由生态
English summary: README.en.md | License: MIT | 移植到其他行业: docs/PORTING.md
一、怎么启动
方式 A:挂进 DSH GUI 左下角(推荐)
左侧栏底部、Settings 上方会有一个 茶 直播工作台 入口(和 ◈ 自媒体工作台 并排)。
点它在右侧栏打开工作台,会话继续占中栏;再点一次收起。
这是通过 DSH 插件接入的,见 第六节。刷新 GUI 页面即可看到。
方式 B:独立进程
双击 启动.bat,或者:
cd F:\dsh\直播工作台
node server/index.mjs
然后打开 http://127.0.0.1:8788。换端口:node server/index.mjs 8899。
两种方式用的是同一份后端实现(server/lib/handler.mjs),没有两套路由。
依赖说明:只需要 Node ≥ 20。PPTX 导出需要 Python + python-pptx, 页面左侧栏会显示「PPT 引擎:python-pptx x.x」;如果显示不可用, 其它模块照常能用,只有 PPTX 导出会失败(在
data/settings.json里可以指定pythonPath)。
二、十个模块
| # | 模块 | 作用 | 备注 |
|---|---|---|---|
| 1 | PPT + 口播稿生成 | 选题 → 大纲 → 幻灯片 → 逐字稿 → 导出 PPTX | 本项目的重点 |
| 2 | 山头 / 茶品知识库 | 44 个山头 + 38 条茶品知识,可增删改 | 内容底座,建议按实际口径调整 |
| 3 | AI 知识库问答(RAG) | 问一句,检索知识库后回答,每句带出处;库里没有会直说"不知道" | 需要配对话模型 key |
| 4 | AI 助手(Agent) | 说一句需求,模型自己决定调哪些工具;改数据需人工确认后才执行 | 需要配对话模型 key |
| 5 | 提词器 + 弹幕问答 | 全屏提词器;弹幕问题自动生成可直接念的回答 | 功效类问题会被拦截并合规改写 |
| 6 | 复盘建议 · 合规检查(可折叠) | 直播前的红线核查 + 直播后的数据复盘 | 点标题展开/收起 |
| 6.1 | 复盘 + 切片建议 | 填数据 → 派生指标、问题诊断、切片建议、下场清单 | 分段记录是切片建议的来源 |
| 6.2 | 合规违禁词检查 | 244 词条 + 17 条正则 + 10 条语境规则,三层检查 | 支持白名单与一键替换建议 |
| 7 | 今日情报采集 | 手动录入或抓 RSS,自动打标签 → 转成直播选题 | 没网也能用,抓取失败不影响其它模块 |
| 8 | 批量生成及方案对比(可折叠) | 一次出多份候选,再挑最合适的那一份 | 点标题展开/收起 |
| 8.1 | 批量生成方案 | 勾选多个山头 → 一次出多份方案 + 口播稿;先「预检」看会生成几份 / 跳过几份 | 纯本地计算,秒级,不花 token |
| 8.2 | A/B 方案对比 | 同一款茶几种讲法,并排看指标(密度 / 内容占比 / 合规),选一个 | 纯本地计算;会校验"事实是否一致" |
编号 = 侧栏从上到下第几个。想调顺序就改
web/app.js里的MODULES数组,再按位置重排num。
模块 3 / 4 需要对话模型 API key(在
data/settings.json或环境变量里配)。 没配也不影响其它模块(1、2、5、6.1、6.2、7、8.1、8.2)—— 它们是纯本地逻辑,不联网。模块 8.1 / 8.2 是"效率"模块,不需要模型 key:批量生成一次出多份方案, A/B 对比同一款茶几种讲法并排看指标。详见 README-efficiency.md。
还只有接口、界面没做的:配额看板(
GET /api/quota)。
需要账号密码吗?—— 默认不需要
默认是本地单用户模式:双击 启动.bat 打开就是完整界面,没有登录这一步。
左下角会显示「本地模式(未开启认证)」。
什么时候才需要账号:只有当工作台需要开放给他人使用时(多个茶商 / 多个员工、数据互不可见)。 开启方式:
# 1) 建一个自己的账号(密码自己定,8 位以上)
node scripts/create-user.mjs owner --role approver --admin --password <密码>
# 2) 带认证启动
$env:WORKBENCH_AUTH = "required"
node server/index.mjs
# 然后打开 http://127.0.0.1:8788/login 登录
不想自己建也行:第一次带认证启动时,如果系统里一个账号都没有, 会自动创建
admin并把随机密码打印在启动日志里(只显示一次)。
忘了密码怎么办(本地工具,不需要邮箱找回):
node scripts/create-user.mjs owner --password 新密码 # 重置指定账号密码
node scripts/create-user.mjs # 看现有账号列表
node scripts/create-user.mjs owner --delete # 删掉重建
关掉认证、回到"不用登录":不要设 WORKBENCH_AUTH 直接启动即可。
(注意:如果租户注册表里有多个启用租户,无认证启动会被安全护栏拒绝——这是故意的。)
对外演示与分享
按"接收方需要做什么"分三种方式。
| 方式 | 接收方需要做什么 | 适用场景 |
|---|---|---|
| ① 同一局域网访问 | 在手机浏览器输入地址 | 现场演示、给身边人看 |
| ② 分发源码 / GitHub | 安装 Node 并自行启动 | 同行参考实现、代码评审 |
| ③ 部署到公网 | 打开链接即可 | 需要一台服务器,成本最高 |
① 同一局域网
双击 启动-局域网.bat,窗口会打印可用地址:
→ 局域网(同一 WiFi 的手机 / 别的电脑,在地址栏输下面这个):
http://192.168.3.2:8788 (以太网)
http://172.24.96.1:8788 (vEthernet) ← 虚拟网卡,一般不用这个
手机连接同一个 WiFi 后,在浏览器访问 192.168.* 开头的地址即可;127.0.0.1 仅在本机有效。
⚠ 安全提醒:局域网模式且未开启认证时,同一网络内任何人均可读取并修改数据。 对外演示前应先创建账号,并以认证模式启动。 Windows 防火墙首次启动会弹窗询问,需选择"允许访问"。
② 分发源码 / GitHub
仓库内容为源码,不是可直接访问的在线界面:接收方需安装 Node 并启动项目,才能看到界面。
- ✅ 适合:参考实现、代码评审
- ❌ 不适合:希望"打开链接即见成品"(后者见方式 ③)
不应直接推送工作目录 —— data/ 含真实经营数据(价格、话术、账号)。
使用导出脚本生成干净副本:
node scripts/export-shareable.mjs # → _share/直播工作台-源码/
导出脚本会:
- 只复制代码 + 文档 + 示例数据(示例知识库来自
seed/,非真实经营数据) - 排除
data/tokens.json/data/users.json(凭据)、output/(导出物)、迁移备份 - 在副本中生成
怎么跑起来.md - 当前实测:136 个文件、3.1 MB(压缩后 1.44 MB);副本可独立启动并加载 44 个山头
分发方式(脚本不触碰 git,也不联网):
# 打包分发
Compress-Archive -Path "_share\直播工作台-源码" -DestinationPath "$env:USERPROFILE\Desktop\直播工作台.zip"
# 或发布到 GitHub(在副本目录内执行)
cd _share\直播工作台-源码
git init
git add .
git status # 提交前确认不含真实数据
git commit -m "init"
git remote add origin <仓库地址>
git push -u origin main
.gitignore 已覆盖 data/tenants/、tokens.json、users.json、output/。
③ 部署到公网
需要一台云服务器(或已备案域名),以 HOST=0.0.0.0 启动,并配置反向代理与 HTTPS。
必须启用认证,否则等同于把数据公开在互联网上。建议在确有远程演示需求时再做。
三、模块 1 是怎么工作的
1. 输入
场次主题、主播、品牌、场次类型(新品首发 / 大促逼单 / 知识科普 / 老客答谢 / 常规带货)、 受众、讲法风格、计划时长,以及一到五款茶。
一场直播通常是「过款」节奏,所以支持多款茶。多款茶不只是方便, 它直接决定长场次的内容厚度:每款茶都会有自己的山头档案、品鉴、工艺、产品、价格、凭证段落。
2. 生成逻辑(离线规则引擎,不依赖大模型)
场次类型 → 决定启用哪些段落(PURPOSE_FLOW)
权重分配 → 每个段落拿到多少分钟
段落内容 → 知识库字段 + 直播运营模板 → 幻灯片视觉内容
时长倒推 → 段落分钟数 × 240字/分钟 → 需要多少字 → 从内容块和扩写引擎取稿
代码位置:server/lib/deck/
blocks.mjs— 20 个直播段落模板(开场钩子、山头档案、山场逻辑、品鉴、工艺、冲泡、避坑、对比、产品、凭证、价格、库存、答疑、下单、收尾…),每个段落同时产出幻灯片内容和口播稿expand.mjs— 扩写引擎:知识库字段逐条展开 + 同产区山头横向对比 + 直播间通用节拍。带收尾句变体库和逐行去重,保证长场次不重复build.mjs— 编排:段落规划、幻灯片拆分、页面去重、稿子填充、时间轴
生成是确定性的:同一个 seed 重复生成结果完全一致,便于对比改参前后的结果。
3. 时长与稿子的关系
工作台区分两个概念,别混淆:
- 逐字稿覆盖:主播照着念的时间占全场比例。60 分钟单款茶大约 70%,二款茶约 90%
- 排满率:逐字稿 + 互动环节提词,合计覆盖全场,一般 90–98%
逐字稿没填满的部分,会自动生成互动环节计划(弹幕答疑、现场冲泡演示、盲品对比、老客反馈、产地素材、福利环节…), 每个环节都带可直接念的提示句,提词器里会一起滚动。这是刻意设计:直播不可能是全程念稿。
单款茶做 90 分钟以上,逐字稿覆盖率会下降。想内容更厚,就多加几款茶。
4. 两条输出路径
同一份 DeckSpec 可以输出成两种东西,内容只生成一次:
| 输出 | 版式/风格 | 可编辑 | 适合 |
|---|---|---|---|
PPTX(build_pptx.py) | 工作台内置的茶主题编辑设计 | ✅ 真文本框 | 要给运营改稿、要走 PowerPoint |
| HTML 幻灯片(视觉风格可选) | 所选视觉风格 | ❌(HTML) | 直播现场投屏、离线放、有演讲者视图 |
4.1 PPTX 版式(内置)
16:9、墨绿 + 米白 + 茶金,13 种版式:封面 / 要点 / 卡片 / 清单 / 对比 / 步骤 / 答疑 / 价格 / 产品 / 行动 / 收尾
(品鉴与开场复用卡片版式)。
版面走的是编辑设计那一路,不是「色块卡片 + 彩色圆点」的模板感:
- 细分隔线、幽灵序号、拉开字距的小标签,标题 31pt / 正文 15-16pt(直播投屏够大)
- 封面是墨绿渐变 + 参数化绘制的茶叶叶形纹样(
add_leaf(),用 freeform 按正弦轮廓采样, 局部坐标必须用 EMU,否则坐标会被取整成整数英寸、轮廓碎成阶梯) - 卡片与清单内容按实高垂直居中,短文案不会缩在上沿、下面空一大片
4.2 HTML 幻灯片 + 视觉风格选择器(重点)
模块 1 的「生成方案」按钮上面有一排视觉风格卡片,选哪个就按哪个风格输出 (在「导出」标签页里生成,也可在那里换风格重出,不用重新生成方案):
| 风格 | 出处 | 一句话 |
|---|---|---|
| 电子杂志 × 电子墨水 | guizang-ppt-skill 风格 A · 森林墨 | 衬线大字 + 暖白纸感,山林人文调 |
| 瑞士国际主义 | guizang-ppt-skill 风格 B · 克莱因蓝 | 无衬线 + 单一高饱和强调色,信息驱动 |
| 宋版 · 因果链 | huashu-design · 杉浦康平逻辑定制 | 黑白为骨 + 宋体大字,把因果讲成结构 |
视觉风格是渲染期选择,不进 DeckSpec —— 所以同一份方案换风格不用重新生成。
架构:server/lib/deck/html/
shell.mjs—— 舞台 / 翻页 / 缩放 / 进度 / 打印 / 基准字号,所有风格共用song.mjs/magazine.mjs/swiss.mjs—— 每个风格只提供css+slide()两样CONTRACT.md—— 新风格要满足的契约(含踩过的坑)index.mjs—— 风格注册表;ready:false的风格在 UI 上禁用,不假装能选
HTML 幻灯片还能转成可编辑 PPTX(按当前风格):走 huashu-design 的路线 B
(scripts/pptx_from_rendered.py 读渲染后坐标,文字是真文本框不是贴图)。
两个实测出来的前提写在了 server/lib/pptxStyle.mjs 里:必须用 flat 模式的 HTML
(一页一显的 HTML 转出来第 2 页起全白,因为脚本会跳过隐藏元素),且需要 Python 侧 playwright。
两条已经踩过坑的规则:
- 装饰出血要显式标记:PPTX 侧装饰形状用
deco命名、check_layout.py据此跳过; HTML 侧装饰元素用class="deco" + aria-hidden="true",转换时--skip-class deco跳过。 这样刻意的出血和真正的溢出不会混在一起。 - 网页预览和 PPTX 是一对:
web/slides.js+web/styles.css必须和pipelines/build_pptx.py同步改,否则预览就在骗人。预览的基准字号用2.09cqw(1em ≈ 20pt),必须写在.slide > *上 ——cqw用在容器自己身上 会解析到外层(视口),基准字号会从 12px 变成 32px,内容直接撑爆。
每页的口播稿会写进 PPTX 的演讲者备注,PowerPoint 演示者视图里直接能看。
页面取舍也是按直播场景定的:不留「今晚流程」这种对客户没有信息量的页; 每页的小标签(页眉那行金色小字)只在真的能补充信息时才出现,开场页只有标题本身。
5. 导出
- PPTX —
output/pptx/,可直接下载或本地打开 - Markdown 口播稿 —
output/scripts/,分段带时间轴 - 提词器纯文本 — 一行一句,方便导入手机提词器 App
四、知识库需要按业务完善(模块 2)
初始数据是公开参考区间,价格和海拔随行情浮动,请以实际为准。
每个条目带 confidence:high 是产区/口感这类公认特征,medium 是浮动较大的参考值。
字段说明(山头):
| 字段 | 用途 |
|---|---|
terroir | 山场特征,要写因果(土壤/海拔/树龄 → 风味)。这是 PPT 知识页的核心素材 |
sellingPoints | 必须写成能直接念的口语句子,不要书面化摘要 |
pitfalls | 直播间真正的防坑点,会进「避坑指南」段落,是建立信任的主力内容 |
taste / aroma / soup / leaf | 品鉴四维,会同时进卡片页和口播稿 |
brewTips | 冲泡参数,进冲泡段落和答疑 |
改完直接生效,点「生成方案」就会用新数据。页面上也能导出/导回 JSON。
五、合规检查
三层机制:
- 词表(244 条 / 12 类):绝对化用语、医疗保健功效、虚假承诺、迷信、站外导流、投资升值、虚假产地年份…
- 正则(17 条):数字承诺、联系方式、年份虚标、贬低同行…
- 语境规则(10 条):触发词 × 目标词在窗口内共现才算命中,例如「降」+「三高」
两档处理方式:
- 违禁项:计入风险分,必须改
- 提示项(
mode: "warn"):不是违禁词,而是直播中需要准备依据的表述(树龄、产地、年份宣称),按词聚合提示
误报控制做得比较细,因为茶行业有大量正常表达会被朴素匹配误伤:
allowCompounds:最多「最少」这类中性词豁免allowPatterns:第一泡「第三泡」「树龄」「海拔」这类行业正常表达用正则豁免- 白名单:UI 上一键把误报词加进白名单
实测:第一泡快出汤,老班章这款茶最多泡十次 零命中;
全国最低价,能降三高 全部命中。
六、作为 DSH 插件接入左下角入口
工作台同时是一个 DSH 插件(plugin/dsh-live-workbench/),所以它能像「自媒体工作台」
那样挂在 GUI 侧栏底部。分两个半边:
| 半边 | 文件 | 做什么 |
|---|---|---|
| 宿主 | dist/plugin.js | 用 webServer.register 把工作台挂到前缀路由 /live-workbench |
| 客户端 | client/client.js | 往插槽 sidebar.footer.action 注册左下角那一行;点它在右侧栏开页签 |
两边都只用 DSH 已有的服务,不碰模型也不碰会话:宿主只 inject: ['webServer'],
客户端只 inject: ['slots','sidebarRightTabs','sidebarRight']。
一份实现,两种挂载点
工作台的 HTTP 实现在 server/lib/handler.mjs,带 basePath 参数:
- 独立进程 →
basePath: '',页面挂在/ - DSH 插件 →
basePath: '/live-workbench'
因此前端全部使用相对 URL。这是硬约束:任何以 / 开头的资源引用在子路径下
都会打到站点根,取不到接口。
客户端包为什么是手写的
DSH 的 client-modules 契约是:包的 package.json 声明 dsh.client.platform = "web",
并用 exports["./client"] 指向客户端产物;产物必须是
window.__ModuleLoader__.load({ id, factory }) 包装的 CJS 模块,
而且 id 必须等于包名 —— 模块表用包名当行 id,取件地址从它推导。
这就是一层包装加一个模块,没有引入 TypeScript / tsdown 构建链的必要,
所以 client/client.js 是手写的:改完保存即生效,不需要编译。
接入与卸载
在 profile 补丁里加一条即可(当前已写入 desktop 与 web 两个 profile):
- insert:
- id: live-workbench
name: 'file:///F:/dsh/直播工作台/plugin/dsh-live-workbench/dist/plugin.js'
- 客户端的左下角入口需要刷新一次网页才出现
- 卸载 = 删掉这几行(或改成
disabled: true),再刷新页面
用干净的 file:// URL,不要加 ?v=2 之类的查询串:带查询串会让包名解析异常,
同一个插件被加载两次并报 duplicate route(web profile 里的 media-studio 就是这么坏的)。
让 HMR 监听工作台仓库(否则改后端不生效)
默认 hmr.config.root 是 []:配置文件被监听,源码模块监听是关闭的,而 profile
重组只认 profile 目录与 linked-package 目标里的文件。本插件在 F: 盘别处,不会因为
patch 引用了它就进入 watch root。
所以在 profile 补丁里再加一条(desktop 与 web 都已写入):
- id: hmr
disabled: false
config:
base: 'file:///F:/dsh/直播工作台/'
root:
- 'plugin/dsh-live-workbench'
- 'server'
base必须带结尾斜杠,它只是相对路径的锚点root才决定监听什么——只写base而不写root时,源码监听仍然是关闭的 (base bundle 给的默认root是[]),这一点很容易踩root里写相对路径(相对base):plugin/dsh-live-workbench覆盖宿主入口与客户端产物,server覆盖 handler 及其所有静态 import 的兄弟模块- 插件注册 URL 保持稳定(不要带查询串)
- 这套配置下源码编辑本身就是重载触发,不要再靠碰
cordis.patch.yml
怎么验证真的生效(scripts/verify-hmr.mjs 会把两种重载分开测):
node scripts/verify-hmr.mjs
--- 用例:入口 handler(server/lib/handler.mjs) ---
✓ 本模块已重新求值,随后观察到激活
--- 用例:依赖 paths.mjs(handler 静态 import 的兄弟) ---
✓ 本模块已重新求值,随后观察到激活
✓ 两个采样模块均已重新求值,随后观察到路由激活。
脚本临时加入独立的模块求值探针,结束时移除;
--keep可保留探针供排查。 每个用例必须先记录自己的求值标记,再观察到activated。仅增加日志行数、activate failed或上一个用例恢复文件引发的重载,都不足以通过。 结果只覆盖这两个采样模块,不保证其他文件、所有 profile 或浏览器客户端已刷新。
依据:
deepseek-ai/deepseek-harnessDiscussion #10774 里社区开发者 Caldera 的两次回复 (commit687f282的hmr.config.root/profileRoots();「base锚定相对路径、root选择监听范围」;以及 Node 22.23.4 / 26.12.1 上对「改被 import 文件的 URL 不会改它 静态 import 的 URL」的实测)。上述root收窄方案与「入口/依赖两类重载都通过」是在 打包版 Desktop 上实测得到的。
怎么确认插件真的加载了
插件激活时会往 plugin/dsh-live-workbench/.activation.log 追加一行。
这是刻意做的「可自证」:不用翻宿主日志,看这个文件就知道宿主有没有加载它。
也可以直接探路由:
Invoke-WebRequest http://127.0.0.1:<端口>/live-workbench/api/health -UseBasicParsing
改了代码怎么生效
分两种情况,别混淆:
| 改了什么 | 插件模式(GUI 里) | 独立模式 |
|---|---|---|
pipelines/build_pptx.py(PPT 版式) | 立刻生效 | 立刻生效 |
web/*(前端、预览) | 刷新网页即可 | 刷新网页即可 |
client/client.js(左下角入口) | 刷新网页(HMR 替换客户端模块) | 不适用 |
server/**(后端引擎) | 配好 hmr(base + root)后自动重载(见上一节) | 重启服务即可 |
为什么之前后端必须重启:Node 的 ESM 缓存按解析后的 URL 记账。早期实现用
「动态 import + ?v=timestamp」去穿透缓存,但这只刷新了被 import 的那一个文件:
handler.mjs?v=2 会重新求值 handler.mjs,而它静态 import 的十几个
server/lib/*.mjs 兄弟仍然读旧实例 —— 对本项目这种多文件依赖,等于没重载。
现在的做法:插件改成静态 import server/lib/handler.mjs,让 HMR 顺着静态
依赖链替换;同时在 profile 里配 hmr 的 base + root,把仓库纳入监听范围
(只配 base 不够,root 才决定监听什么)。
静态 import 的代价是 handler 求值失败会让插件加载直接报错(而不是被 catch 住),
这是刻意的「失败要响」。
PPT 版式之所以例外,是因为渲染器是 python 子进程:每次导出都重新读
build_pptx.py,不走 Node 的模块缓存。
开发这套东西时,用独立模式(
node server/index.mjs)迭代最快; 改完再重启 DSH 同步到 GUI。
七、目录结构
直播工作台/
├─ server/ 后端(Node 内置模块,零依赖)
│ ├─ index.mjs 独立进程入口(只负责监听端口)
│ └─ lib/
│ ├─ handler.mjs ★ 全部路由与静态资源,带 basePath,两种挂载共用
│ ├─ kb.mjs 知识库读写与检索
│ ├─ compliance.mjs 合规检查器
│ ├─ intel.mjs 情报采集
│ ├─ qa.mjs 弹幕问答
│ ├─ review.mjs 复盘与切片
│ ├─ ppt.mjs PPTX 导出桥(自动探测 Python,异步不阻塞宿主)
│ └─ deck/ ★ 生成引擎
├─ plugin/dsh-live-workbench/ DSH 插件
│ ├─ dist/plugin.js 宿主半边:注册 /live-workbench 前缀路由
│ └─ client/client.js 客户端半边:左下角入口 + 右侧栏页签
├─ pipelines/
│ ├─ build_pptx.py python-pptx 渲染器(15 种版式)
│ └─ check_layout.py 版式越界检查
├─ web/ 前端(原生 HTML/CSS/JS,无构建步骤)
├─ data/ 所有数据与状态(JSON,可直接查看/备份)
├─ output/ 导出产物:pptx/ 演示文稿、scripts/ 口播稿
└─ scripts/ 验证脚本
八、自检
# 不需要启动任何服务
node scripts/smoke.mjs # 引擎 + 合规 + PPTX 导出 + 版式越界
node scripts/copy-smoke.mjs # 文案体检(半角引号 / 版式覆盖)
node scripts/style-smoke.mjs # 三种视觉风格 × 两个场次:结构 + 字号 + 对比度 + 逐页像素
node scripts/style-pptx-check.mjs # 按风格转出的可编辑 PPTX:画布 / 真文本框 / 形状不越界
node scripts/mount-smoke.mjs # 子路径挂载(插件模式的前提)
node scripts/plugin-smoke.mjs # 插件契约(宿主装配 + 客户端插槽注册)
node scripts/preview-smoke.mjs # 预览 HTML 结构(标签配平 / 根类名 / idx·notes 位置)
# 需要先启动服务
node scripts/api-smoke.mjs # 全部 API 端点(含视觉风格与 HTML 导出)
smoke.mjs 覆盖:知识库完整性、合规零误报/真违规必抓、五个时长档位的覆盖率与排满率、
同页重复句、视觉重复页、版式内容缺失、PPTX 结构、版式越界、幂等性。
preview-smoke.mjs 是给预览加的结构测试:预览是拼字符串生成的 HTML,
标签一旦不配对,浏览器会自行「纠正」,典型后果是后面的幻灯片被嵌进第一张、
整页宽度爆炸 —— 而看截图很难发现。它同时锁住两条约束:
根节点类型类必须是 st-*(s-* 留给正文布局类,同名会把根节点变成那个布局),
以及 .idx / .notes 必须在 .slide 内部(否则备注面板永远不显示)。
style-smoke.mjs 跑两个场次而不是一个:没有任何单一 purpose 能产出全部 13 种版式
(newlaunch 缺 compare、knowledge 缺 price/product,并集才全),只跑一场会留覆盖盲区 ——
这个盲区实测存在过。它还会检查页内不得出现页码(页码由外壳圆点承载),
这条规则曾经被两个风格各自违反过一次。
style-pptx-check.mjs 专门验转换产物:HTML 看着没问题 ≠ 转出的 PPTX 没问题。
实测踩过「翻页式 HTML 转出第 2 页起全白」和「页脚横跨整页导致文本框 1932pt > 画布 1920」,
两者都只在转换产物里才看得见。
plugin-smoke.mjs 会把客户端半边塞进一个模拟的 __ModuleLoader__ 里跑一遍,
确认它真的注册到了 sidebar.footer.action(左下角插槽)和右侧栏两个 seat,
并验证「点一下开、再点一下关」的行为 —— 不用启动 DSH 就能拦住契约错误。
九、已知边界
- 情报源的 RSS 抓取依赖网络,且不同站点格式差异大;抓不动就用手动录入,不影响其它功能
- 弹幕问答是规则匹配 + 知识库取词,不是大模型对话。回答质量取决于知识库写得多好
- 复盘指标只使用录入的数据。没有累计观看数,因此不套用 GPM,改用人均产出与每小时产出
- PPTX 排版由 python-pptx 生成,不保证与 PowerPoint 像素级一致;导出前建议在网页预览里过一遍
- 生成稿是「可用的初稿」而不是终稿。合规检查过了不代表文案就一定没问题,涉及功效、产地、年份的表述请自行核实依据
- GUI 里是用 iframe 嵌工作台的:工作台的样式表是全局的(
:root/body/button), 直接挂进外壳 DOM 会把整个 GUI 重绘,所以刻意用 iframe 隔离 - 改
server/下的后端代码后,插件模式需要重启 DSH 才生效(ESM 模块缓存按 URL 记账, HMR 只重新调用apply()、不会重新求值嵌套模块);build_pptx.py与前端不受此限,见第六节