← Back to home@Dogwind221

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)。

二、十个模块

#模块作用备注
1PPT + 口播稿生成选题 → 大纲 → 幻灯片 → 逐字稿 → 导出 PPTX本项目的重点
2山头 / 茶品知识库44 个山头 + 38 条茶品知识,可增删改内容底座,建议按实际口径调整
3AI 知识库问答(RAG)问一句,检索知识库后回答,每句带出处;库里没有会直说"不知道"需要配对话模型 key
4AI 助手(Agent)说一句需求,模型自己决定调哪些工具;改数据需人工确认后才执行需要配对话模型 key
5提词器 + 弹幕问答全屏提词器;弹幕问题自动生成可直接念的回答功效类问题会被拦截并合规改写
6复盘建议 · 合规检查(可折叠)直播前的红线核查 + 直播后的数据复盘点标题展开/收起
6.1复盘 + 切片建议填数据 → 派生指标、问题诊断、切片建议、下场清单分段记录是切片建议的来源
6.2合规违禁词检查244 词条 + 17 条正则 + 10 条语境规则,三层检查支持白名单与一键替换建议
7今日情报采集手动录入或抓 RSS,自动打标签 → 转成直播选题没网也能用,抓取失败不影响其它模块
8批量生成及方案对比(可折叠)一次出多份候选,再挑最合适的那一份点标题展开/收起
8.1批量生成方案勾选多个山头 → 一次出多份方案 + 口播稿;先「预检」看会生成几份 / 跳过几份纯本地计算,秒级,不花 token
8.2A/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。

五、合规检查

三层机制:

  1. 词表(244 条 / 12 类):绝对化用语、医疗保健功效、虚假承诺、迷信、站外导流、投资升值、虚假产地年份…
  2. 正则(17 条):数字承诺、联系方式、年份虚标、贬低同行…
  3. 语境规则(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-harness Discussion #10774 里社区开发者 Caldera 的两次回复 (commit 687f282 的 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 与前端不受此限,见第六节