# sema-up — 一键部署 sema 栈

`sema-up` 把 sema 智能体服务端(`@sema-agent/server`)连同数据面(PG+MinIO)、配置中心
(sema-registry)、可选 git 服务(Gitea)一键装到单机 docker 或 k8s 上。

```bash
# npm 分发姿势(package.json bin 已挂)
npx -p @sema-agent/server sema-up --profile=full-single --gateway=https://api.deepseek.com/v1 \
  --model=deepseek-v4-flash --model-key=sk-… -f site.env --yes
# 或直接跑仓内脚本
deploy/sema-up/sema-up.sh
```

## 三入口 × 部署形态矩阵

三个入口进的是**同一引擎**(同一交互协议、同一 site.env 词表):

| 入口 | 用法 | 适合 |
|---|---|---|
| TTY 交互 | `sema-up.sh`(无参) | 人在终端,一问一答(每问带「为什么问」) |
| JSON 协议 | `sema-up.sh --interactive=json` | `sema cloud deploy` 等上层 UI 渲染决策点 |
| 无人值守 | `sema-up.sh -f site.env --yes` | CI/脚本;缺必填直接 fail-loud 不猜 |

| 形态(--profile) | 装什么 | 底座 | 状态 |
|---|---|---|---|
| `full-single` | 引擎+PG+MinIO(+registry+git) | 单机 docker compose | 可用 |
| `data-center` | 只数据面 PG+MinIO(+registry+git),agent 在别处连 | 单机 docker compose | 可用 |
| `local-plus` | 本机只跑 agent,连远端 data-center 数据面 | 单机 docker compose | 可用 |
| `full-cluster --kube=existing` | 全栈上已有 k8s(kubeconfig 指向;kind/k3d/云商托管) | helm chart(`chart/`) | 可用 |
| `full-cluster --kube=k3s` | 本机装 k3s 单机再 helm 装栈 | k3s+helm | 可用(Linux) |
| `full-cluster --kube=machines` | 多机 ssh 装配 k3s HA(embedded etcd) | `cluster-up.sh` | 可用(≥3 机) |

**配置中心(sema-registry)各档默认带上**;`--registry=false` 显式关。例外:`local-plus`
默认不带(数据面在远端,配置中心应随数据面部署;本机再起一个=两个配置中心)。

## git 面三态(--git)

| 态 | 含义 | registry 集成 |
|---|---|---|
| `none`(默认) | 跳过 | — |
| `provision` | 一键自建 Gitea(引导管理员+access token+持久卷) | **自动**:创建 OAuth app,registry 可用 Gitea 账号登录 |
| `connect --git-url=… --git-token=…` | 接已有 git 服务 | Gitea:自动建 OAuth app;GitHub:无法自动(registry 只支持 Gitea OAuth),summary 打印待人工项 |

自动化的边界(诚实声明):sema-registry 没有「git 服务器」持久化配置——能自动接的只有
OAuth 登录(env:`GITEA_BASE_URL`/`GITEA_OAUTH_CLIENT_ID`/`GITEA_OAUTH_CLIENT_SECRET`);
git 凭据按契约**不进配置中心**(worker 侧自管)。自动建 OAuth app 失败时不装成功,
summary 会打印手动接法。

## 全部 flag

| flag | 默认 | 说明 |
|---|---|---|
| `--profile=P` | 交互问 | `full-single`/`data-center`/`local-plus`/`full-cluster` |
| `--users=U` | `single` | `single`(超管 turnkey)/`multi`(身份隔离 REQUIRE_PRINCIPAL=true) |
| `--region=R` | `auto` | `cn`/`global`;auto=对 aliyun/npmjs 探测延迟 |
| `--project=NAME` | `sema` | compose project 名;并行试验/多套共存用 |
| `--dry-run` | — | 只打印执行计划,不写 .env、不起容器 |
| `--yes` | — | 无人值守(全默认;缺必填 fail-loud) |
| `--interactive=json` | — | 机器可读交互协议 |
| `-f FILE` | — | site 配置(env 形式;`machines.yaml` 走固定形状 mini-parser) |
| `--port=N` | 8090 | 引擎端口;`--pg-port`(5432)/`--minio-port`(9000)/`--minio-console-port`(9001)同理 |
| `--gateway=URL` | — | 模型网关 baseURL(OpenAI 兼容);带 agent 的档必需 |
| `--model=ID` | `deepseek-v4-flash` | 模型 id |
| `--model-key=K` | — | 模型 API key(也可 -f 里 MODEL_API_KEY) |
| `--sandbox=LANE` | kvm 有→`kata` | `kata`/`e2b-cloud`/`runc`(full-single) |
| `--registry=BOOL` | `true`(local-plus `false`) | 配置中心网站开关 |
| `--registry-image=REF` | — | sema-registry 镜像(registry=true 必需) |
| `--git=MODE` | `none` | `none`/`provision`/`connect` |
| `--git-url=URL --git-token=T` | — | connect 态的已有服务(Gitea/GitHub/任意三方 git,自动识别;证书不校验) |
| `--pg-url=URL` | — | 外接第三方 PG(`postgres://user:pass@host:port/db`);给了就不部署本地 PG,装配前真连检 |
| `--s3-endpoint=URL --s3-access-key= --s3-secret-key=` | — | 外接第三方 S3(兼容协议:MinIO/OSS/COS/R2…);不部署本地 MinIO,装配前 ls+建桶检(`--insecure`) |
| `--data-root=DIR` | 多盘机自动选大盘 | 数据卷 bind 到指定盘;首装落定不漂移(换盘=teardown-volumes 后重装) |
| `--git-http-port=N` | 3300 | 自建 Gitea HTTP 口;`--git-ssh-port`(2222)同理 |
| `--gitea-image=REF` | `gitea/gitea:1.24` | 自建 Gitea 镜像 |
| `--pg-host= --pg-password= --minio-endpoint= --minio-password=` | — | local-plus 的远端数据面(data-center summary 里有) |
| `--kube=MODE` | 交互问 | full-cluster:`existing`/`k3s`/`machines` |
| `--kubeconfig=PATH` | `$KUBECONFIG`→`~/.kube/config` | `--kube=existing` 用 |
| `--teardown` / `--teardown-volumes` | — | 卸载(留数据 / 连卷删) |
| `--skip-preflight` | — | 跳过预检(不建议) |

site.env 词表 = 上表的 env 形式(`PROFILE`/`SEMA_SERVER_IMAGE`/`SEMA_REGISTRY_IMAGE`/
`GIT_MODE`/`GIT_URL`/`GIT_TOKEN`/`MODEL_API_KEY`/`E2B_API_KEY`/`IMAGE_PULL_USER`/`IMAGE_PULL_TOKEN` …);
命令行显式参数赢过文件。k8s 路径额外:`STORAGE_CLASS`(缺省用集群默认 SC)、`NAMESPACE`(sema)。

## 性能/磁盘:测量→建议(不强求)→硬底线

preflight 实测 CPU/RAM/磁盘并分两档:**推荐值**(full:4c/8G/80G——4C8G 能部署全部)以下只给
WARN+建议(data-center 档/local-plus/外接 S3+DB/换大盘),照样部署;**硬底线**(full:2c/3G/40G,
data-center:2c/2G/20G)以下 BLOCK——确实无法稳定部署,提前警告不推进。多盘机自动扫描真实
块设备挂载,存在明显更大的数据盘(>2× 根盘可用)时自动把数据卷 bind 过去(`--data-root` 可
覆盖;macOS 只建议不自动)。k8s 路径同理:建议 3 节点(HA),少于 3 节点按缩放形态跑;
单节点 allocatable <1c/2G 才拦。凡带 URL 的外接面(PG/S3/git)装配前都做真连接检测,
一律忽略证书(内网自签是常态),验的是协议兼容与可达。

## 镜像从哪来

- **server**:`SEMA_SERVER_IMAGE`(`ghcr.io/sema-agent/sema-server:<tag>`,public 匿名可拉;
  CN 兜底=自建镜像 registry。SWR 已弃用)。
- **registry**:`SEMA_REGISTRY_IMAGE`(GHCR/Hub 的 `sema-registry` 镜像,迁移中,公开仓匿名可拉;
  SWR 已弃用;仅换 private 仓时才需 k8s 路径凭据=env `IMAGE_PULL_USER`/`IMAGE_PULL_TOKEN`)。
- 公共镜像(postgres/minio/gitea)直接拉;国内网络不通时配 dockerd `registry-mirrors`。

## 凭据纪律

全部凭据生成一次、落 `~/.sema-up/<project>/.env`(0600),重跑**绝不轮换**;
summary 里管理员密码只显示一次(新生成那次),不进日志不进 git。

## 验收

```bash
deploy/sema-up/smoke.sh --project=sema          # 数据面直验+registry/gitea+引擎
deploy/sema-up/smoke.sh --url=http://host:8090 --token=$SERVICE_AUTH_TOKEN   # 远程引擎面
```

## 常见错误排查

| 症状 | 原因 | 下一步 |
|---|---|---|
| `⛔ registry=true 需要 sema-registry 镜像` | 各档默认带 registry 了 | `--registry-image=…` 或 `--registry=false` |
| `compose up 失败` + 拉镜像超时 | 镜像仓不可达/私仓没登录 | `docker login`;国内配 registry-mirrors;`docker compose -p <project> logs` |
| preflight `port-XXXX occupied` | 端口被占 | `--port`/`--pg-port`/`--git-http-port` 换口,或停占用进程 |
| preflight `BLOCK os` | 不在支持面 | Debian 12/13、Ubuntu 22.04/24.04、Rocky 9/10、macOS+docker |
| `没有可用的 compose` | docker CLI 无 compose 插件 | 装 Docker Desktop/OrbStack 或 `brew install docker-compose` |
| `docker-credential-desktop not found` | 卸载 Docker Desktop 后 `~/.docker/config.json` 残留 `credsStore` | 删掉该文件里的 `"credsStore": "desktop"` 行(或临时 `DOCKER_CONFIG` 指向干净目录) |
| k8s:`集群没有默认 StorageClass` | PVC 会永久 Pending | `kubectl get sc` 后 `STORAGE_CLASS=<名>` 重跑 |
| k8s:PG 一直不 healthy | 镜像在拉/PVC Pending/内存不足 | `kubectl -n sema describe cluster sema-pg` |
| k8s:server `CreateContainerConfigError` | runner-token 复制晚于 pod 创建 | 重跑 kube-up(幂等);已内置先复制后等待 |
| Gitea OAuth app 创建失败 | token 缺 `write:user` scope | summary 的待人工项有手动接法 |
| smoke `auth 门` FAIL | SERVICE_AUTH_TOKEN 未生效=公网裸奔 | 立刻查 .env 与容器 env |
| 模型面 402/401 | 余额耗尽/key 无效 | 充值/换 key 后重跑 smoke |
| NodePort 不可达(kind/远程) | kind 节点是容器,宿主到 NodePort 不通 | `kubectl -n sema port-forward svc/sema-server 8090:8090` |

## 文件地图

```
sema-up.sh            入口(决策/预检/凭据/装配/验证/summary)
preflight.sh          单机预检(--json;BLOCK 带原因+建议)
smoke.sh              验收(project 模式=数据面直验;--url 模式=引擎面)
kube-up.sh            k8s 单机两态(existing kubeconfig / 本机 k3s)→ helm chart
cluster-up.sh         多机 ssh k3s HA 装配(machines.yaml)
chart/                helm chart(server/CNPG PG/MinIO/registry/gitea/沙箱 RBAC)
stack/compose.yaml    单机 Layer 1(profiles: data/agent/registry/git)
machines.example.yaml 多机清单模板
lib/parse-machines.py machines.yaml mini-parser(标准库零依赖,fail-loud)
```
