Star 历史趋势
数据来源: GitHub API · 生成自 Stargazers.cn
README.md

OpenTV

说一个想法,开拍一整季。

一块无限画布配一个懂创作的 Agent 精雕精品,一套工作流无人值守跑量产 —— 把一句话做成一部持续更新的短剧。

OpenTV 落地页


这是什么

OpenTV 是规划 + 生产连续型短视频内容的工具。一个项目 就是一块无限画布: 你把想法/剧本丢进来,在画布上用节点(脚本、图片、视频、音频…)推进创作,右侧的 Agent 作为副驾帮你建节点、改内容、答疑优化。

两种产品形态,刻意分开做:

形态取向状态
创作画布 — 无限画布 + 节点 + 侧边 Agent质量 > 效率,人在环内在建,可用
生产管线 — 剧本→生图→视频→发布 自动跑效率 > 质量,无人值守设计已定稿,尚未开工

已经是真的:登录注册(邮箱验证码 + 邀请码门禁)、个人资料、无限画布、统一节点系统、 侧边 Agent(真流式对话、节点引用、「/」技巧引用)、脚本节点的 LangGraph 分镜管线、 Evolink 真实出图/出片、素材库、关键帧抽取、Remotion 合成渲染。 还是假数据:市场页与账单页(lib/*-data.ts)。

完整清单见 docs/features-and-todo.md,架构见 docs/specs/2026-07-24-canvas-dual-form-architecture.md

技术栈

  • 前端:Next.js 16(App Router / Turbopack)· React 19 · TypeScript · Tailwind CSS v4 · shadcn + Base UI · Motion · GSAP · three.js(落地页 3D)
  • 画布 / AI:@xyflow/react(React Flow v12)· AI SDK v7 · @langchain/langgraph(脚本节点编排)· Remotion(合成渲染,独立子进程)· ffmpeg-static(抽帧)· 模型走 Evolink 中转
  • 服务端:better-auth(会话/邮箱验证码/组织)· Prisma + PostgreSQL · Redis(限流 + 会话缓存)· 阿里云 OSS(S3 兼容协议)· Resend(验证码邮件)

目录速览

app/            页面与 API 路由(canvas/[projectId] 是全屏工作台;(app) 是带侧边栏的应用外壳)
components/     canvas/ 画布与节点 · landing/ 落地页 · auth/ 登录注册 · ui/ 基础组件
lib/            script/ 脚本节点引擎 · ai/ Evolink 与模型注册表 · agent/ 侧边 Agent ·
                media/ 抽帧 · video/ Remotion 渲染入口 · assets/ 素材库 · invite.ts 邀请码门禁
prisma/         数据库 schema 与迁移
remotion/       Remotion 合成(子进程渲染,不进 Next 打包图)
docs/           设计文档与功能清单

本地开发

1. 需要什么

  • Node.js 22+npm(本项目只用 npm,锁文件是 package-lock.json)
  • PostgreSQL 16Redis 7(用 Docker 起最省事,见下)
  • 几个外部服务的密钥:阿里云 OSS(存素材)、Resend(发注册验证码邮件; 本地可不配,验证码会打印在终端里)。Evolink 的 key 不配在这里 —— 登录后到「设置 → AI 服务」页面填,按工作区存进数据库(见下面「AI 服务密钥」一节)

2. 起数据库和 Redis

docker run -d --name opentv-db   -p 5432:5432 \
  -e POSTGRES_PASSWORD=postgres -e POSTGRES_DB=opentv postgres:16
docker run -d --name opentv-redis -p 6379:6379 redis:7-alpine

3. 配环境变量

cp .env.example .env

.env 里至少要填这几项:

变量说明
DATABASE_URL上面那个库,如 postgresql://postgres:postgres@localhost:5432/opentv?schema=public
REDIS_URLredis://localhost:6379
BETTER_AUTH_SECRET会话签名密钥,用 openssl rand -base64 32 生成
BETTER_AUTH_URL本地 http://localhost:3000;线上填你的对外地址
INVITE_CODES注册邀请码(见下节)。本地留空则注册免填
OSS_*阿里云 OSS 的 region / key / bucket,生成的素材传这里
RESEND_API_KEY发验证码邮件。本地可留空(打印到终端);生产必填,否则启动报错

4. 装依赖、建表、跑起来

npm install
npx prisma migrate deploy     # 按 prisma/migrations 建表
npm run dev                   # → http://localhost:3000

注册时验证码会打印在跑 npm run dev 的终端里(没配 Resend 的情况下)。

5. 改代码之后

npm run lint        # eslint
npm run typecheck   # TypeScript(生产构建会因类型错误失败,先过这个)
npm test            # vitest
npm run build       # 生产构建(⚠️ 别在 npm run dev 还开着的时候跑,两者共用 .next)

改了 prisma/schema.prisma 一定要配一个迁移:npx prisma migrate dev --name <名字>


AI 服务密钥(在页面上配,不在 .env)

Evolink 的 key 按工作区(组织)存在数据库里,由用户自己在「设置 → AI 服务」页面填 —— 谁的画布花谁的额度。.env 里没有 EVOLINK_*,任何环境都不回落读它。

  • 加密落库:AES-256-GCM,密钥用 HKDF 从 BETTER_AUTH_SECRET 派生 (lib/crypto/secret.ts)。接口只回显尾 4 位,明文永不出服务端。 ⚠️ 换掉 BETTER_AUTH_SECRET 后旧密钥解不开,到页面重填一次即可。
  • 可选覆盖:图像/视频基址、文本基址、默认文本模型(高级选项里),留空用供应商默认值。
  • 测试连通:页面上那个按钮会真发一次极小的请求(几个 token),因为格式对不代表 key 有效 —— 过期、欠费、模型无权限都只有调用时才知道。
  • 没配会怎样:所有生成类接口回 400 并提示去设置页;读画布、看历史消息等不受影响 (lib/credentials/route.tsrequireProjectAi)。
  • 怎么流到调用点:路由入口取一次组织凭证并进入请求上下文 (lib/ai/credentials.ts,AsyncLocalStorage),LangGraph 图节点内部、 分离执行的后台生成任务都能拿到同一份,不必层层传参。

注册需要邀请码

生成能力是花钱的(每个新账号都能调用 Evolink),所以线上不接受自助注册: 建号必须带一个服务端认可的邀请码。

配置:.envINVITE_CODES,逗号分隔可配多个(发给不同的人,便于单独作废):

INVITE_CODES="OPENTV-ZS4V-D7GR,OPENTV-GKQQ-N7RM"

生成一个新码:

node -e "const{randomInt}=require('crypto');const A='ABCDEFGHJKLMNPQRSTUVWXYZ23456789';const g=n=>Array.from({length:n},()=>A[randomInt(A.length)]).join('');console.log('OPENTV-'+g(4)+'-'+g(4))"

作废:把那串从 INVITE_CODES 里删掉并重启服务(已注册的账号不受影响)。

门禁的三条规则(实现在 lib/invite.tslib/auth.tsdatabaseHooks.user.create.before):

  1. 建号的唯一咽喉在服务端:不管走哪条路(密码注册 / 邮箱验证码 / 以后接的社交登录), 新建用户都要过邀请码校验 —— 不是靠前端藏个输入框。
  2. 生产环境 fail closed:NODE_ENV=production 时即使忘配 INVITE_CODES, 也不会对全网敞开,而是一律拒绝注册(页面提示「本站暂未开放注册」)。
  3. 验证码只能登录已有账号,不能顺带注册;陌生邮箱来要码会静默返回, 既不发信(不花钱)也不泄漏该邮箱是否注册过。

校验忽略大小写、空格和连字符,所以 opentv zs4v d7gr 也能过。


Docker 部署

镜像里包含 Next 生产构建、Prisma 迁移工具,以及 Remotion 渲染要用的无头 Chrome 依赖。 docker-compose.yml 一起拉起 应用 + PostgreSQL + Redis

1. 服务器上准备

需要 Docker 24+ 和 Docker Compose v2。把代码拉到服务器,然后:

cp .env.example .env
# 必填:BETTER_AUTH_SECRET / BETTER_AUTH_URL(你的对外地址) / INVITE_CODES
#       OSS_* / RESEND_API_KEY(Evolink 的 key 在页面上配,不在这里)
vim .env

或者从飞书配置表拉(改配置不用再登服务器,见下面「配置放在飞书表格里」):

cp .feishu.env.example .feishu.env && vim .feishu.env   # 只填 3 行飞书凭证
npm run env:pull -- --write                             # 表格 → .env

.env 里的 DATABASE_URLREDIS_URL 不用改:compose 会用容器网络里的 db / redis 覆盖它们(compose 的 environment 优先于 env_file),所以同一份 .env 本地开发也能继续用。带引号的值 compose 会自动去引号,照常写。

另外这几个变量只有 compose 读,应用本身不读(.env.example 末尾一节里都列了, 默认值够用就不用管):

变量默认作用
POSTGRES_PASSWORDpostgres数据库密码,compose 用它拼容器内的 DATABASE_URL
APP_PORT3000对外映射的宿主机端口(容器内部固定 3000,别写成 PORT)
NODE_IMAGEdocker.1ms.run/...node:22-bookworm-slim基础镜像,海外换回 node:22-bookworm-slim
NPM_REGISTRYhttps://registry.npmmirror.comnpm 源,海外换回 https://registry.npmjs.org
APT_MIRRORmirrors.aliyun.comDebian 源,留空则用官方 deb.debian.org

2. 启动

docker compose up -d --build
docker compose logs -f app        # 看启动日志(会先跑 prisma migrate deploy 再起服务)

好了之后访问 http://服务器地址:3000,用邀请码注册第一个账号。

3. 更新版本

git pull
docker compose up -d --build      # 重建镜像;容器启动时自动应用新迁移

4. 常用运维

docker compose ps                                   # 看状态(app 带健康检查)
docker compose logs -f app                          # 应用日志
docker compose exec app node ./node_modules/prisma/build/index.js migrate status
docker compose exec db pg_dump -U postgres opentv > backup-$(date +%F).sql   # 备份
docker compose down                                 # 停(数据在 volume 里,不会丢)

5. 几个坑

  • HTTPS:容器只听 3000 的 HTTP,线上前面套一层 Nginx/Caddy 做 TLS 和反代, 并把 BETTER_AUTH_URL 设成 https://你的域名 —— 否则登录 cookie 和邮件里的回调地址会对不上。
  • Mac 上给 x86 服务器打包:本机是 arm64,要显式指定平台 docker build --platform linux/amd64 -t opentv:latest .(或者直接在服务器上 build)。
  • 下载源默认走国内加速,不用额外配置:基础镜像 docker.1ms.run、npm registry.npmmirror.com、apt mirrors.aliyun.com。实测差别很大:官方源装 ffmpeg 要 4 分半、npm ci 撞上超时能拖到 20 分钟;镜像源下 npm ci 约 30 秒、apt 秒级。 (ffmpeg 用系统包而不是 ffmpeg-static 下载的二进制,也是为了绕开经常超时的 GitHub Releases。) 海外服务器想换回官方源,在 .env 里覆盖:
    NODE_IMAGE=node:22-bookworm-slim
    NPM_REGISTRY=https://registry.npmjs.org
    APT_MIRROR=
    直接 docker build 时对应 --build-arg NODE_IMAGE=... --build-arg NPM_REGISTRY=... --build-arg APT_MIRROR=
  • 视频渲染要能下无头 Chrome:构建时会试着预装,失败不影响启动;但首次渲染视频时 Remotion 会去 Google 的地址拉 chrome-headless-shell,服务器出不去网就渲染不了 —— 画布上生图/生视频(走 Evolink)不受影响。
  • 别把社交登录随手打开:配了 GOOGLE_* / GITHUB_* 之后,那两个入口的新账号 仍然会被邀请码门禁挡住(报错在 OAuth 回调页,提示不如表单里友好),已有账号登录不受影响。
  • .env 里别用 $:compose 会把 $VAR 当变量插值(要写成 $$);值两侧的引号 compose v2 会自动去掉,可以照常写。
  • 镜像不小(含 Remotion 的无头 Chrome 和 ffmpeg),首次构建按网速可能要十几分钟。

6. 机器规格:2c2g 够不够(全是实测,不是估算)

先给结论:画布那条线 2c2g 完全够;视频合成(Remotion)2c2g 扛不住,要 4G 内存。

内存实测(按【进程树】精确统计,不是按进程名匹配——那样会把本机的 VS Code / Edge 也算进来):

场景峰值内存耗时峰值在谁身上
稳态(app 137 + PG 28 + Redis 10)~175MB;加系统与 Docker 约 500MB
合成:占位画面 240 帧 → 1080×1920828MB14.3sffmpeg 编码 710MB
合成:真实视频 3×1080p 素材 / 360 帧 → 1080×19201536MB39.1s无头 Chrome 798MB + Rust compositor 305MB
同上,输出降到 720×12801427MB(只省 7%)32.4s同上——Chrome 的开销来自解码 1080p ,与输出尺寸无关
同上,帧缓存 192MB → 64MB1593MB(无效,噪声内)40.8s同上

算一笔账:真实合成 1.5GB + 基线 0.5GB = 2.0GB,正好把 2GB 的机器撑满 → OOM (而且报错只有 退出码 137,极难查)。降分辨率、压帧缓存都试过,都不是杠杆—— 1.4-1.6GB 是当前实现的地板。

所以按用途选:

并发的账(同一条真实素材,只改并发):

并发峰值内存耗时4GB 机器上总占用
11536MB39.1s2.0GB / 4GB
2(compose 默认)2106MB23.6s2.6GB / 4GB ← 甜点
42631MB15.9s3.1GB / 4GB(只剩 900MB,同时有人生图就吃紧)

按机器选:

机器建议
4c4g够,合成也能用。REMOTION_CONCURRENCY=2(compose 已默认)。注意 4GB 机器 free -h 实际显示约 3.6Gi(内核/固件占 0.4G),按 3.6G 算:基线 0.5 + 合成 2.1 = 2.6G,余量约 1.0G;想再快可以试 4,但那样只剩约 0.4G,不建议
2c2g只做画布创作够用(生成都在 Evolink 云端算,本机基本不耗);别开合成——真实合成 1.5GB + 基线 0.5GB 正好撑满 2GB → OOM(报错只有 退出码 137)

⚠️ 不管并发调多少,渲染必须串行(已在 lib/video/render.ts 里排队):并发 2 的两条 渲染撞一起就是 4.2GB,4GB 机器照样崩。队列是让上面这些数字成立的前提。

不管哪种,这三条都要做:

  1. 构建时别同时跑渲染:next build 峰值 2-4GB。4GB 机器构建得动(CI 部署就是在服务器 上构建的,内存不够会自动停掉 app 腾内存重试),但构建和视频合成撞一起必 OOM。 2GB 机器别在上面构建——无 swap 必被 OOM Killer 杀。
  2. 加 4GB swap(命令见上文)。
  3. 渲染已强制串行(lib/video/render.ts 队列)+ 并发默认 1 (REMOTION_CONCURRENCY 可调)。并发默认值实测 1418MB → 809MB(占位场景), 这是唯一真正有效的内存杠杆;两条渲染撞一起必崩,所以宁可排队。

已做但没有实测收益的两处,留着只为可预测,别当成省内存手段:产物包磁盘缓存 (省一次 esbuild、耗时 -1s,内存基本不动)、帧缓存钉死 192MB(对峰值无影响)。


自动部署(merge 到 main 就上线)

推到 main → GitHub Actions 先跑检查,检查过了才部署:把这份代码 rsync 到服务器, SSH 进去就地构建、换容器、等健康检查转绿。

merge dev → main
   ↓
GitHub Actions(免费 runner)
   ├─ 检查:prisma validate / typecheck / lint / test   ← 红了就到此为止,不部署
   └─ 部署:rsync 源码到服务器
      ↓ SSH
服务器:docker compose build app && up -d app
        ├─ 容器启动时自动 prisma migrate deploy
        └─ 轮询健康检查,转绿才算这次部署成功

流程定义在 .github/workflows/deploy.yml

📋 第一次上线照着这份一步步敲:docs/deploy-runbook.md (服务器打底 → 送配置 → 配 Secrets → 让 CI 跑第一次 → 上域名 HTTPS → 日常运维 + 排错表)

为什么在服务器上构建

先试过「更专业」的那条路:runner 构建镜像推腾讯云 TCR,服务器只 pull。实测首次部署 26 分钟,而构建本身早就完了 —— 时间全花在两处:GHA 缓存导出 339 秒,以及 runner 在墙外往国内镜像仓库推 ~1.5GB。runner 每次都是全新机器,Docker 层缓存只能靠 GHA 缓存来回搬,搬的成本就把构建的便宜抵掉了。

服务器就地构建:层缓存留在本机(下次通常只重跑 next build 那一层),代码增量只有几十 KB, 零跨境传输。首次 10-20 分钟,之后 3-6 分钟。代价是构建期间占服务器 CPU/内存, 可能有几分钟不可用 —— 我们接受。

配 GitHub Secrets

仓库 → Settings → Secrets and variables → Actions。不需要镜像仓库凭证,只要 SSH:

Secret
SSH_HOST / SSH_USER / SSH_KEY服务器 IP、登录用户、为 CI 单独生成的私钥全文
SSH_PORT可选,默认 22
DEPLOY_DIR服务器上放代码与 .env 的目录,如 /opt/opentv

CI 专用密钥这样生成(别把你自己的私钥交给 CI):

ssh-keygen -t ed25519 -C "opentv-ci" -f ~/.ssh/opentv_ci -N ""
ssh-copy-id -i ~/.ssh/opentv_ci.pub <用户>@<服务器IP>
cat ~/.ssh/opentv_ci            # 全文(含 BEGIN/END 两行)贴进 SSH_KEY

源码是 rsync 过去的,服务器不需要仓库权限。本来更自然的做法是让服务器 git pull 私有仓库,但 CloudNook 组织禁用了 deploy key,而往服务器塞 PAT 比只读 key 更糟; 国内机器直连 github.com 也慢。rsync 还有个额外好处:传过去的正是检查通过的那份代码。

服务器上准备一次(之后就全自动)

服务器上只有 .env 要你手动放 —— 代码和 docker-compose.yml 由 CI 每次同步过去, .env 在 rsync 的排除名单里,放上去就一直在。

sudo apt update && sudo apt install -y docker.io docker-compose-v2 rsync
sudo usermod -aG docker $USER && newgrp docker

# ⚠️ 必做:配 Docker 镜像源,否则拉 postgres/redis 会卡死(国内连不上 Docker Hub)
sudo mkdir -p /etc/docker && sudo tee /etc/docker/daemon.json >/dev/null <<'EOF'
{
  "registry-mirrors": ["https://mirror.ccs.tencentyun.com"],
  "log-driver": "json-file",
  "log-opts": { "max-size": "20m", "max-file": "3" }
}
EOF
sudo systemctl restart docker
# 腾讯云机器实测:mirror.ccs.tencentyun.com 走内网 0.07s,拉 postgres:16 共 37 秒。
# 它只能当 daemon 的 registry-mirror,不能当镜像名前缀。配好后 .env 里写官方镜像名:
#   POSTGRES_IMAGE="postgres:16"  REDIS_IMAGE="redis:7-alpine"  NODE_IMAGE="node:22-bookworm-slim"

sudo mkdir -p /opt/opentv && sudo chown $USER /opt/opentv && cd /opt/opentv

# 方式一:本机直接 scp 一份上来(最省事)
#   scp .env <用户>@<服务器IP>:/opt/opentv/.env
#
# 方式二:从飞书表格拉(需要服务器上有 node)
cat > .feishu.env <<'EOF'
FEISHU_APP_ID=cli_xxx
FEISHU_APP_SECRET=xxx
FEISHU_ENV_WIKI_URL=https://xxx.feishu.cn/wiki/xxx
EOF
npm run env:pull -- --write

# 加 2G swap 当保险(腾讯云 Ubuntu 镜像通常自带一块,先 swapon --show 看看)
sudo fallocate -l 2G /swapfile && sudo chmod 600 /swapfile
sudo mkswap /swapfile && sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

.env 必填:BETTER_AUTH_SECRETBETTER_AUTH_URL(必须等于你实际访问的地址, 否则登录报 Invalid origin)、INVITE_CODESOSS_*RESEND_API_KEYPOSTGRES_PASSWORD(⚠️ 只在数据卷首次初始化时生效,第一次 up -d 前就要定好)。

之后的每次上线与回滚

git checkout main && git merge dev && git push    # 就这一步

首次冷构建实测 18 分 57 秒,之后有层缓存通常 3-6 分钟(只重跑 next build 那一层)。 部署脚本在服务器上是后台跑的(docker/deploy.sh),CI 只轮询结果 —— 因为我们真被 一次 SSH 断连掐死过 19 分钟的构建。

回滚不用重新构建:每次部署 CI 会给镜像额外打一个 opentv-app:<commit前12位> 标签 (最近 5 个留着),几秒切回去:

ssh <用户>@<服务器IP> && cd /opt/opentv
docker images opentv-app                                   # 看可回滚的标签
APP_IMAGE=opentv-app:abc123def456 docker compose up -d app  # 命令行变量优先于 .env

⚠️ 回滚只换代码,不回退数据库迁移。带破坏性 schema 变更的那次上线,回滚前先想清楚。


配置放在飞书表格里

一张两列的飞书多维表格(第一列 key、第二列 value)是线上配置的唯一事实来源每次部署都会自动把表格拉成服务器上的 .env —— 改线上配置 = 改表格 + 触发一次部署, 不用再 ssh 上去 vim .env

本地开发不参与这套:npm run dev 用你本机的 .env,和表格互不干涉。

cp .feishu.env.example .feishu.env   # 填 FEISHU_APP_ID / FEISHU_APP_SECRET / FEISHU_ENV_WIKI_URL
npm run env:pull                     # 预览表格与当前 .env 的差异(值打码,不落盘)
npm run env:pull -- --write          # 写 .env(原文件备份成 .env.bak)
npm run env:push                     # 反向:把 .env 推上表格(加 --dry 只看)

两道防呆,都是踩出来的:

  • pull --write 会校验必填项(REQUIRED_KEYS:BETTER_AUTH_SECRET / BETTER_AUTH_URL / POSTGRES_PASSWORD / INVITE_CODES / OSS_* / RESEND_API_KEY)。表格里少填一行 = 线上少一项配置, 所以缺了就拒绝写盘、保留原文件。POSTGRES_PASSWORD 尤其致命:一丢,compose 退回默认密码, 应用直接连不上库。
  • push 默认跳过 BETTER_AUTH_URL / DATABASE_URL / REDIS_URL。这三项本地线上本就不同 (本地 localhost、线上公网地址),不跳的话本地跑一次 push 就把线上地址覆盖成 localhost, 线上登录立刻报 Invalid origin。真要改就在表格里直接改,或加 --force

服务器上不用装 node:部署脚本借一个 node 镜像跑完就扔 (docker run --rm --user $(id -u):$(id -g) ... node scripts/feishu-env.mjs pull --write)。 --user 不能省 —— 不加会让 .env 变成 root 所有,之后普通用户改不动。 拉取失败不拦部署,保留服务器上原有的 .env 继续跑。

飞书应用要开 bitable:app(多维表格读写)和 wiki:wiki:readonly(读知识库), 发布版本后还要把这个应用加进那张表:打开表格 → 右上「···」→「添加文档应用」。 少这一步会报 91403 —— 有 API 权限 ≠ 有这张表的权限。

三条边界,别踩:

  1. 表格的可见范围就是生产密钥的可见范围。 里面是 OSS AccessKeySecret、 BETTER_AUTH_SECRET、数据库密码 —— 这页只给自己,别开组织内可见,别发链接分享。
  2. 它没有消灭「服务器上放密钥」,只是把一整份 .env 换成两行飞书凭证。
  3. 拉取发生在部署时,不在 docker build 时。 密钥进了镜像层就等于泄漏 (docker history 就能翻出来),而 .env 本来就是 compose 在运行时注入的, 构建阶段根本不读。FEISHU_* 自身不进表格,pull 也不会覆盖掉本地那几行。

许可

私有项目,暂未开源许可。落地页用到的 3D 模型来自 Quaternius(CC0)。

关于 About

OpenTV·灵感画布 — 短视频内容的策划与生产工具:一个项目 = 一张无限画布,节点创作 + 侧边 Agent 副驾

语言 Languages

TypeScript91.0%
HTML4.6%
JavaScript1.9%
Python1.3%
CSS0.4%
Shell0.4%
Dockerfile0.3%

提交活跃度 Commit Activity

代码提交热力图
过去 52 周的开发活跃度
399
Total Commits
峰值: 121次/周
Less
More

核心贡献者 Contributors