minimax.ink
A Field Notebook on AI-Made Things

零依赖搭一个视频生成 Demo:Node 18 + 原生 ES Modules + 单文件代理


零依赖搭一个视频生成 Demo

我之前拿 MiniMax 视频生成 API 搓过一个 Demo,周末重新跑了一遍、读了一下源码、做了次安全审计,顺手把这篇复盘写下来。

不是为了吹”多牛”——它克制得刚好。1000 行 Node 代码、零 node_modules、零打包器,把”异步视频生成”这件事拆得清清楚楚。

但我自审的时候也发现了三处安全坑——README 第 296-301 行我自己标的”无鉴权 / 监听 0.0.0.0 / Key 存 localStorage”之外,还漏了三处。这篇文章两件事都讲。


这东西是什么

简单说:它是 MiniMax 视频生成 API 的前端 Demo。

零运行时依赖。Node 18+ 自带 fetch,前端用原生 ES Modules,clone 下来 node server.mjs 就能跑。没有 node_modules 那个黑洞

下面这张是我在本地把服务跑起来后截的实拍:API Key 输入框、模型/分辨率/时长三个下拉、prompt 输入、首帧图上传区、生成按钮,下面挂着两条已经成功生成并缓存到本地的历史(中式八球台球宝贝 + 森林小鹿)。

MiniMax Video Demo 运行界面:参数区、prompt、两条已生成历史

仓库:https://github.com/HChaoHui/MiniMax-Video


三个值得记的工程细节

1. 劫持原生 <select>value setter

<select> 太丑,想替换成自定义的,又得支持键盘 ↑↓ Enter Esc Space

常规做法:加一个状态层(把所有用 sel.value = 'x' 的地方改成 setState('x')),然后让自定义 UI 监听 state。

我当时选了一个更骚的解法——

Object.defineProperty(realSelect, 'value', {
  set(v) {
    realSelect.dataset.value = v;
    // 同步自定义 UI
  },
  get() { return realSelect.dataset.value; }
});

劫持原生 <select>value setter。所以 sel.value = 'x' 这类代码不用改,既走原生行为,又自动同步自定义 UI。整个项目里别的 JS 一行不用动。

我回头看这个写法时愣了一下——我自己写大概率会再搞一个状态层把这件事兜住。

2. 写锁

历史记录是 JSON 文件,浏览器每次操作都要写。我自己写的时候就被这个问题绊过——两个标签页同时开着、或者点得太快,就会出现半写入。

我最后用的写法:

let writeQueue = Promise.resolve();
function writeHistory(data) {
  writeQueue = writeQueue.then(async () => {
    const tmp = HISTORY_FILE + '.tmp';
    await fs.writeFile(tmp, JSON.stringify(data, null, 2));
    await fs.rename(tmp, HISTORY_FILE);   // 原子替换
  });
  return writeQueue;
}

先写临时文件,再 rename 原子替换,Promise 串行化避免并发。

简单粗暴但够用。对 1000 行的 JSON 引入 native 依赖是过度设计——SQLite 那种原生模块编译一遍三分钟,值不值的自己掂量。

3. 错误码对照表

这个是写代码时没想到的

我当时把 API 文档截图丢给大模型,它不光把对接代码写了,还顺手从文档里抠了一个错误码表出来塞进 README:

错误码含义
1002触发限流
1004API Key 错误
1008余额不足
1026内容敏感
2013参数异常
1027输出内容错误

这种”我自己没打算写、但模型主动加的细节”,在 M3 这种原生多模态的模型上特别常见——视觉理解 + 长文档解析 + 代码生成是它的原生场景,扫一眼文档就把这种”上下文里的隐性信息”挖出来。

但也有反向坑,我也踩了:模型会瞎猜文档里没写的东西。错误码那块它就”补全”了几个看起来合理但实际不存在的码。遇到不确定就标 TODO,不要瞎猜——这条提示要主动加。


顺手审计:三处我没标的安全坑

我自己 README 第 296-301 行只标了三条安全风险(没有用户认证、监听 0.0.0.0、API Key 存 localStorage)。但还漏了三处,我周末重跑一遍才看出来。

坑 1:/api/* 代理是裸的

后端有个 /api/* 代理,功能是把浏览器请求转发到 api.minimaxi.com 并附上用户的 API Key。

if (req.url.startsWith('/api/')) return proxyRequest(req, res, req.method);

proxyRequest 只校验 X-API-Key有没有,不校验是谁给的。配合 Access-Control-Allow-Origin: *,任何能访问 3000 端口的人都能拿自己的 API Key 走你的代理——如果你部署在公网,Key 会被盗刷。

修法:加一个 ADMIN_TOKEN 环境变量 + 中间件,所有 /api/* 必须带 X-Admin-Token 头。三行代码的事。

坑 2:DELETE 类接口无鉴权

// DELETE /api/history — 清空全部
// DELETE /api/orphan_videos — 全清
// POST   /api/orphan_videos/clean — 选择性清理

这三个接口没有任何鉴权。任何能访问端口的人 curl -X DELETE 就能把你所有历史一键清空。

修法:同上,ADMIN_TOKEN 中间件覆盖所有 /api/*

坑 3:readBody 无大小限制

function readBody(req) {
  return new Promise((resolve, reject) => {
    const chunks = [];
    req.on('data', c => chunks.push(c));
    // ...
  });
}

不限 Content-Length,不限累计字节数。攻击者发个 10GB 的 POST /api/history 就能把你 Node 进程内存打爆。简单限个 MAX_BODY = 10 * 1024 * 1024 就够。


这类项目适合谁

如果你:

那这种”单文件后端 + 原生 ES Modules 前端 + 零依赖”的玩法挺合适的。

反过来,大型工程、严格 PR 流程、强合规项目,这种玩法现阶段还不合适。AI 在快速迭代,工程化背书也不够


收个尾

代码差不多 1000 行,README 300 多行,全部开源。

clone 下来加个 API Key 就能跑。但先加鉴权再放到公网——别嫌烦,上面那三坑你躺中一个就够喝一壶的。

仓库:https://github.com/HChaoHui/MiniMax-Video

如果你也想搓一个 MiniMax 视频生成的应用,可以从这里开始

如果你也在用大模型搓项目,欢迎留言讲讲你的玩法,交流一下。

想自己试一下? 第一次上手 minimax 不需要太多准备——这里注册一个账号,就能开始做点东西。