中文 · English
Markdown is boring, show me the HTML.
SMTH(Show Me The HTML)是一个面向 CLI agent 的单用户 HTML artifact 存放服务。 它把每个 artifact 以原始 HTML 加 JSON 元数据的形式保存在文件系统上,并自带一个 轻量的「侧栏 + 画布」界面,在沙箱 iframe 中预览 artifact。
不想手动 build/run?把下面这段 prompt 交给一个能 SSH 登录目标 Linux 主机的 CLI agent:
展开部署 prompt
Deploy this SMTH repo to my Linux server.
Inputs:
- SSH target: <user@host or ssh alias>
- Public/LAN URL I will use in the browser: <http(s)://...>
- Port to listen on: <port, default 8080>
- Install root: <remote path, default ~/apps/smth>
- Read access boundary: <LAN / Tailscale / reverse proxy auth / other>
Requirements:
1. Inspect the remote host first: OS, architecture, available ports, systemd
user support, existing SMTH install, and whether the requested port is free.
2. Build the correct Linux binary from this repo. For x86_64 use:
`GOOS=linux GOARCH=amd64 go build -o smth-server ./cmd/smth-server`.
For ARM64 use `GOOS=linux GOARCH=arm64`.
3. Deploy as:
`<install-root>/releases/<git-sha>/smth-server`
`<install-root>/releases/<git-sha>/static/`
`<install-root>/shared/data/`
`<install-root>/shared/smth.env`
`<install-root>/current -> releases/<git-sha>`
4. Generate `SMTH_API_KEY` on the server if it does not exist. Store it only in
`<install-root>/shared/smth.env` with mode 600. Do not commit it or print it
unless I explicitly ask for it.
5. Create a user-level systemd service if possible:
`smth-server --port <port> --data <install-root>/shared/data --static <install-root>/current/static --max-size 2097152 --public-read`
Use `--public-read` only because the browser UI needs read access for SSE and
artifact preview; protect reads with my stated LAN/Tailscale/reverse-proxy
boundary. Write endpoints must still require `X-API-Key`.
6. Enable and start the service, then verify:
- service is active
- `GET /` returns 200
- `GET /api/artifacts?limit=1` works from the protected read boundary
- unauthenticated `POST /api/artifacts` returns 401
- authenticated `POST /api/artifacts` can create a small smoke artifact
- raw HTML can be read back
- logs do not leak `SMTH_API_KEY`
7. Leave me with the URL, service name, install paths, key file path, and the
exact commands for status, logs, restart, and rollback.
8. After the server side is verified, set up the publisher side (the machine or
client that will run the CLI agent / publish script). Ask me which key-storage
method I want before doing anything:
(a) Embed in skill (recommended, works on web/client environments without a
shell): substitute the real base URL and key into a PRIVATE copy of the
skill. The committed template must keep the `$SMTH_API_KEY` placeholder;
the filled-in copy must never be committed, git-ignored or kept outside
the repo. The base URL is not secret and may be written into the skill.
(b) Environment variable (only if my publisher side has a persistent shell):
write `SMTH_API_KEY` into my shell profile or `~/.config/smth/env`
(mode 600, outside any git repo); the skill references `$SMTH_API_KEY`
and reads it at runtime.
Do not print the key value, and do not commit any file containing it.
Do not expose the write API key in iframe URLs or query strings. If you change
the repo while deploying, run `go test ./...`, `node --check static/app.js`,
and `docker build -t smth:local .` if Docker files changed.
go build -o smth-server ./cmd/smth-serverSMTH_API_KEY=change-me ./smth-server --port 8080 --data ./data --public-read --max-size 2097152构建镜像并以命名卷运行:
docker build -t smth:local .
docker run -d \
--name smth \
-p 8080:8080 \
-e SMTH_API_KEY=change-me \
-e SMTH_PUBLIC_READ=true \
-e SMTH_BASE_URL=http://localhost:8080 \
-v smth-data:/data \
smth:local或使用 Compose:
SMTH_API_KEY=change-me \
SMTH_BASE_URL=https://smth.example.com \
docker compose up -d --build容器以非 root 用户(UID/GID 65532)运行,artifact 保存在 /data,并从
/app/static 提供内置前端。如果用绑定挂载(bind mount)代替命名卷,请确保宿主
目录对 65532 可写。在 Compose 文件里设置 SMTH_HOST_PORT 可以改变对外发布的
宿主端口。
.
├── cmd/smth-server/ # Go 单二进制服务端及测试
├── data/ # 运行时 artifact 存储
├── design/ # 原始设计交付归档
├── docs/assets/ # README 预览图
├── static/ # 由二进制提供的前端文件
├── Dockerfile
├── compose.yaml
├── go.mod
├── SKILL.md # 给 CLI agent 用的发布 skill
├── README.md # 中文(默认)
└── README_en.md # 英文
命令行参数:
--port 8080:HTTP 端口。--data ./data:文件系统存储根目录。--static ./static:前端目录。--public-read:允许未认证的GET和 SSE 读取。--max-size 2097152:原始 HTML 的最大字节数。--base-url https://smth.example.com:可选,创建响应中返回的对外 URL。
容器部署可以用对应的环境变量代替:
SMTH_PORTSMTH_DATA_DIRSMTH_STATIC_DIRSMTH_PUBLIC_READSMTH_MAX_SIZESMTH_BASE_URLSMTH_API_KEY(写入密钥;只从环境变量读取,没有对应的命令行参数)
SMTH 在设计上就是用来托管任意的、不受信任的 HTML/JS 的。下面这些防护都已内置—— 在把实例暴露到 localhost 以外之前,请先读完本节。
- 写入认证。 每个
POST/DELETE请求都必须带X-API-Key: $SMTH_API_KEY, 并以恒定时间(constant-time)比较。如果没有设置SMTH_API_KEY,写入端点一律 返回401。密钥只从请求头读取——通过查询字符串传入会被拒绝,日志里出现的api_key查询参数也会被脱敏。 - 读取认证。 读取(
GET和 SSE)默认也需要密钥,除非启用了--public-read。⚠️ 一旦启用--public-read,所有 artifact 的元数据、原始内容 以及实时事件流,对任何能连到该端口的人都是公开可读的。内置浏览器 UI 不会发送 密钥,所以它只有在--public-read下才能用。请用防火墙、Tailscale、Basic Auth 或反向代理来限定谁能读取——并且绝不要在开启 public-read 的实例上放非公开内容。 - artifact 沙箱。
/a/{id}在响应头里带上Content-Security-Policy: sandbox allow-scripts allow-forms allow-popups allow-modals, 即使作为顶层页面直接打开,也会让 artifact 运行在一个不透明源(opaque origin)里。 因此 artifact 脚本无法读取本应用所在源的数据或本地存储,也无法发起带凭据的同源 API 调用。应用内预览所用的 iframe 带有相同的sandbox属性(不含allow-same-origin)。 - 加固响应头。 应用与 API 响应都带有
X-Content-Type-Options: nosniff和Referrer-Policy: no-referrer(artifact 外链跳转时不会把 artifact URL 泄露给目标站点)。 控制 UI 额外带X-Frame-Options: DENY,禁止被嵌入 iframe。注意 artifact 路由/a/{id}不带X-Frame-Options——它本来就要被应用内的 sandbox iframe 嵌入预览, 其防护改由上面的 CSP sandbox 提供。 - 资源限制。 请求体大小受
--max-size限制;并发 SSE 订阅者上限为 256,超出 的连接会收到503。 - 浏览器 UI 在设计上是只读的。 创建、替换、删除请通过 CLI agent 或脚本携带 写入密钥来完成。
写入密钥(SMTH_API_KEY)能创建、替换、删除任意 artifact,泄露等同于把服务的写
权限交出去。生成一把足够随机的:
openssl rand -hex 32服务端的那份存放方式已在「一键部署」的 prompt 里规定:写入
<install-root>/shared/smth.env,chmod 600,不提交、不打印。下面讲的是发布端
(运行 CLI agent / 调脚本那一侧)该怎么存这把 key。
推荐:把 URL 和 key 写进 skill。 发布端往往是网页端或客户端这类没有持久 shell 环境的地方,读不到环境变量,所以最通用的做法是在部署时把 skill 模板里的 URL 和 key 占位符替换成自己的真值,让 agent 直接加载这份填好的 skill。
$SMTH_API_KEY 这样的占位符);你在本地、或网页端的私有 skill
槽里填上真值使用,那份填好的副本保持私有——加进 .gitignore、放在仓库目录之外、或
干脆只存在不受跟踪的运行环境里,别提交、别分享、别截图。
备选:环境变量。 如果你的发布端确实是带 shell 的机器,也可以把真值留在版本控制
之外,skill 只引用变量名 $SMTH_API_KEY,运行时从环境读取:
# 写进 shell profile,或单独一个仓库外的文件
echo 'export SMTH_API_KEY=<your-key>' >> ~/.zshrc
# 或:
mkdir -p ~/.config/smth && echo 'SMTH_API_KEY=<your-key>' > ~/.config/smth/env
chmod 600 ~/.config/smth/env # 之后在 skill 里 source 它URL 不是机密,无论哪种方式都可以放心写死进 skill。
data/
2026-05-10/
01HXYZ...abc.html
01HXYZ...abc.json
元数据:
{
"id": "01HXYZ...",
"title": "live migration plan v2",
"project": "migration-paper",
"tags": ["plan", "draft"],
"created_at": "2026-05-10T14:23:00Z",
"updated_at": "2026-05-10T14:23:00Z",
"size_bytes": 12345
}artifact ID 使用 ULID。删除是软删除(只在元数据里加上 deleted_at 时间戳);底层
文件仍然留在磁盘上,但对所有读取端点都不可见。
POST /api/artifacts
{
"html": "<!doctype html>...",
"title": "live migration plan v2",
"project": "migration-paper",
"tags": ["plan", "draft"],
"replace": "01HXYZ..."
}replace 是可选的。不带时,SMTH 会新建一个 ULID artifact,返回
{id, url, created_at};带上时,SMTH 会就地覆盖对应 artifact 的 HTML 并更新其
元数据,返回 {id, url, created_at}。
GET /api/artifacts?project=&limit=50&before={ulid}
返回按时间倒序排列的元数据。limit 默认为 50,最大 200。已删除的 artifact 会被排除。
GET /api/artifacts/{id}
返回单个 artifact 的元数据。对不存在或已删除的 artifact 返回 404。
GET /a/{id}
以 text/html 返回原始 HTML,并在 CSP 沙箱下提供(见安全模型)。
既用于 iframe 预览,也用于直接打开 artifact。对已删除的 artifact 返回 404。
DELETE /api/artifacts/{id}
通过在元数据里加上 deleted_at 来软删除 artifact。
GET /api/stream
Server-Sent Events:
new:{id, title, project, created_at}update:{id, updated_at}delete:{id}
服务端每 30 秒发送一次 : heartbeat;客户端带 Last-Event-ID 重连时,会重放
最近 100 条事件。最多同时服务 256 个订阅者,超出时该端点返回 503。
项目名 SMTH 是 Show Me The HTML 的缩写,口号化用自 Linus Torvalds 在 2000-08-25 的 LKML 回复: "Talk is cheap. Show me the code."
本项目积极参与并认可 linux.do 社区。