@h-ai/deploy 0.1.0-alpha.55 → 0.1.0-alpha.56

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # @h-ai/deploy
2
2
 
3
- 自动化部署模块,支持一键将 SvelteKit 应用部署到 Vercel,并自动开通所需的基础设施服务。
3
+ 自动化部署模块,支持两种部署方式:一键将 SvelteKit 应用部署到 Vercel(并自动开通云基础设施),或将应用容器化后通过 SSH 部署到任意 Linux 主机(docker-ssh)。
4
4
 
5
- ## 支持的服务
5
+ ## 支持的部署方式
6
+
7
+ | Provider | 目标 | 说明 |
8
+ | ------------ | --------------- | ---------------------------------------------- |
9
+ | `vercel` | Vercel 平台 | SvelteKit + adapter-vercel,配合云 Provisioner |
10
+ | `docker-ssh` | 任意 Linux 主机 | SvelteKit + adapter-node,容器化后经 SSH 部署 |
11
+
12
+ ## 支持的云服务(Provisioner)
6
13
 
7
14
  | 服务类型 | Provisioner | 说明 |
8
15
  | -------- | ------------- | ---------------------- |
9
- | 部署 | Vercel | SvelteKit 应用部署 |
10
16
  | 数据库 | Neon | PostgreSQL Serverless |
11
17
  | 缓存 | Upstash | Redis TCP + TLS |
12
18
  | 存储 | Cloudflare R2 | S3 兼容对象存储 |
@@ -77,6 +83,105 @@ deploy.credentials.saveAll({
77
83
 
78
84
  > `deploy.scan()` 与 `deploy.credentials.*()` 都是本地文件系统能力,不依赖 `deploy.init()`。
79
85
 
86
+ ## docker-ssh 部署
87
+
88
+ 将应用容器化后通过 SSH 部署到任意 Linux 主机,无需云平台账号。
89
+
90
+ ### 前置条件
91
+
92
+ - **本地**:安装 docker 或 podman(`auto` 优先 podman)。
93
+ - **远程主机**:可 SSH 登录、已装 docker/podman 与 compose、登录用户可运行容器。
94
+ - **应用**:SvelteKit 使用 `@sveltejs/adapter-node`(工程自带的 `Dockerfile` 已满足);缺少时返回 `ADAPTER_MISSING`。
95
+
96
+ ### 使用步骤
97
+
98
+ 1)把 SSH 私钥路径存入凭证文件(不进仓库)。编辑 `~/.hai/credentials.yml`,或用 API:
99
+
100
+ ```typescript
101
+ deploy.credentials.save('HAI_DEPLOY_SSH_KEY', '~/.ssh/hai_deploy')
102
+ ```
103
+
104
+ 2)在 `config/_deploy.yml` 声明目标主机:
105
+
106
+ ```yaml
107
+ provider:
108
+ type: docker-ssh
109
+ ssh:
110
+ host: 10.0.0.20 # 主机 IP 或 ~/.ssh/config 别名
111
+ port: 22
112
+ username: deploy
113
+ identityFile: ${HAI_DEPLOY_SSH_KEY} # 私钥文件路径,省略则用 ssh-agent
114
+ remote:
115
+ baseDir: /opt/hai/apps # 远程部署根目录
116
+ runtime: auto # 远程运行时;auto 优先 podman,其次 docker
117
+ expose:
118
+ type: port
119
+ containerPort: 3000 # 容器内端口(adapter-node 默认 3000)
120
+ hostPort: 18080 # 宿主机端口 → 访问地址 http://10.0.0.20:18080
121
+
122
+ container:
123
+ runtime: auto # 本地构建运行时(可选)
124
+
125
+ services: {} # 启用的服务会自动生成 compose sidecar,见下
126
+ ```
127
+
128
+ 3)部署并验证(CLI 或 API,二者等价):
129
+
130
+ ```bash
131
+ hai deploy # 读取 config/_deploy.yml,输出访问地址
132
+ ```
133
+
134
+ ```typescript
135
+ import { deploy } from '@h-ai/deploy'
136
+
137
+ deploy.credentials.load() // 注入 HAI_DEPLOY_* 环境变量
138
+ await deploy.init(dockerSshConfig)
139
+ const result = await deploy.deployApp('./apps/my-app')
140
+ if (result.success) {
141
+ result.data.url // http://<host>:<hostPort>
142
+ result.data.healthCheck // 'passed'(HTTP 探测成功)
143
+ }
144
+ await deploy.close()
145
+ ```
146
+
147
+ ### 实现逻辑
148
+
149
+ `deployApp()` 对 docker-ssh 依次执行:
150
+
151
+ 1. **扫描**:读取 `package.json` 与 `config/*.yml`,识别 `@sveltejs/adapter-node` 与所需服务及其后端类型。
152
+ 2. **本地构建**:用工程自带的 `Dockerfile` 构建镜像 `hai-<app>:<部署ID>`(podman 追加 `--format docker --jobs=1`)。
153
+ 3. **导出**:`docker/podman save` 导出为 tar。
154
+ 4. **预检**:SSH 连通性 + 远程 `docker/podman version` 与 `compose version`。
155
+ 5. **传输加载**:`scp` 上传 tar → 远程 `load` → 重打为 `localhost/` 本地标签(避免误从 registry 拉取)。
156
+ 6. **生成运行描述**:按扫描结果生成 `compose.yml`(app + sidecar)与 `.env.runtime`(含 `HAI_*` 覆盖变量与凭据),上传到 `<baseDir>/<app>/`,`.env.runtime` 设为 `600`。
157
+ 7. **启动**:`compose up -d`(Compose 项目名固定 `hai-<app>`,重复部署幂等)。
158
+ 8. **健康检查**:轮询 `http://<host>:<hostPort>/`,返回 200 才判定 `ready`。
159
+
160
+ > 镜像、`compose.yml`、`.env.runtime` 落在应用 `.hai/deploy/`(已在模板 `.gitignore` 忽略)。
161
+
162
+ ### compose 按功能生成 sidecar
163
+
164
+ `deploy.scan()` 会识别所需服务及其**实际后端类型**(读取 `_db.yml` / `_vecdb.yml` 等的 `type`),生成匹配的 sidecar 并注入对应 `HAI_*` 覆盖变量:
165
+
166
+ | 扫描到的服务 | 后端 (`type`) | 追加的 sidecar | 注入变量 |
167
+ | ------------ | ------------- | ----------------------------- | ------------- |
168
+ | `db` | `postgresql` | `postgres:16-alpine` | `HAI_DB` |
169
+ | `db` | `mysql` | `mysql:8.4` | `HAI_DB` |
170
+ | `db` | `sqlite` | 无(挂载 `/app/data` 数据卷) | — |
171
+ | `cache` | `redis` | `redis:7-alpine` | `HAI_CACHE` |
172
+ | `storage` | `s3` | `minio` + 初始化 bucket | `HAI_STORAGE` |
173
+ | `vecdb` | `qdrant` | `qdrant/qdrant` | `HAI_VECDB` |
174
+ | `vecdb` | `pgvector` | `pgvector/pgvector` | `HAI_VECDB` |
175
+ | `vecdb` | `chroma` | `chromadb/chroma` | `HAI_VECDB` |
176
+ | `vecdb` | `lancedb` | 无(挂载 `/app/data` 数据卷) | — |
177
+
178
+ - `cache` 为 `memory`、`reach`(邮件/短信)为外部 API,均不生成 sidecar。
179
+ - 若同一服务已配置云 Provisioner(如 Neon 提供 `HAI_DB`),优先用云资源,不再自建 sidecar。
180
+ - sidecar 的密码等敏感值仅写入远程 `.env.runtime`(`600`),不会进入 `compose.yml`。
181
+
182
+ > 安全:`identityFile` 为私钥**路径**(非私钥内容);`strictHostKeyChecking: true`(默认)
183
+ > 采用 accept-new(首次信任、拒绝变更)。首次连接未知主机可先手动 `ssh` 一次。
184
+
80
185
  ## 错误处理
81
186
 
82
187
  ```typescript
@@ -106,6 +211,17 @@ if (!result.success) {
106
211
  | `HaiDeployError.NOT_INITIALIZED` | `hai:deploy:010` | 未初始化 |
107
212
  | `HaiDeployError.ENV_VAR_FAILED` | `hai:deploy:011` | 环境变量失败 |
108
213
 
214
+ docker-ssh 专属错误码:
215
+
216
+ | 错误码 | code | 说明 |
217
+ | -------------------------------------------- | ---------------- | ---------------- |
218
+ | `HaiDeployError.CONTAINER_RUNTIME_NOT_FOUND` | `hai:deploy:015` | 未找到容器运行时 |
219
+ | `HaiDeployError.CONTAINER_BUILD_FAILED` | `hai:deploy:016` | 镜像构建失败 |
220
+ | `HaiDeployError.REMOTE_CONNECT_FAILED` | `hai:deploy:017` | 远程连接失败 |
221
+ | `HaiDeployError.REMOTE_COMMAND_FAILED` | `hai:deploy:018` | 远程命令失败 |
222
+ | `HaiDeployError.IMAGE_TRANSFER_FAILED` | `hai:deploy:019` | 镜像传输失败 |
223
+ | `HaiDeployError.HEALTH_CHECK_FAILED` | `hai:deploy:020` | 健康检查未通过 |
224
+
109
225
  ## 测试
110
226
 
111
227
  ```bash
package/dist/index.d.ts CHANGED
@@ -9,16 +9,112 @@ import { HaiResult, HaiError } from '@h-ai/core';
9
9
  * @module deploy-config
10
10
  */
11
11
 
12
+ /** SSH 连接配置(docker-ssh Provider 使用 OpenSSH 客户端连接远程主机) */
13
+ declare const SshConfigSchema: z.ZodObject<{
14
+ host: z.ZodString;
15
+ port: z.ZodDefault<z.ZodNumber>;
16
+ username: z.ZodString;
17
+ identityFile: z.ZodOptional<z.ZodString>;
18
+ knownHostsFile: z.ZodOptional<z.ZodString>;
19
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
20
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
21
+ }, z.core.$strip>;
22
+ /** 端口直出暴露配置 */
23
+ declare const PortExposeSchema: z.ZodObject<{
24
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
25
+ containerPort: z.ZodDefault<z.ZodNumber>;
26
+ hostPort: z.ZodNumber;
27
+ }, z.core.$strip>;
28
+ /** docker-ssh 部署平台配置 */
29
+ declare const DockerSshProviderConfigSchema: z.ZodObject<{
30
+ type: z.ZodLiteral<"docker-ssh">;
31
+ ssh: z.ZodObject<{
32
+ host: z.ZodString;
33
+ port: z.ZodDefault<z.ZodNumber>;
34
+ username: z.ZodString;
35
+ identityFile: z.ZodOptional<z.ZodString>;
36
+ knownHostsFile: z.ZodOptional<z.ZodString>;
37
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
38
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
39
+ }, z.core.$strip>;
40
+ remote: z.ZodDefault<z.ZodObject<{
41
+ baseDir: z.ZodDefault<z.ZodString>;
42
+ runtime: z.ZodDefault<z.ZodEnum<{
43
+ auto: "auto";
44
+ docker: "docker";
45
+ podman: "podman";
46
+ }>>;
47
+ }, z.core.$strip>>;
48
+ expose: z.ZodObject<{
49
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
50
+ containerPort: z.ZodDefault<z.ZodNumber>;
51
+ hostPort: z.ZodNumber;
52
+ }, z.core.$strip>;
53
+ }, z.core.$strip>;
12
54
  /** 部署平台配置联合类型(后续可扩展更多平台) */
13
55
  declare const ProviderConfigSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
14
56
  type: z.ZodLiteral<"vercel">;
15
57
  token: z.ZodString;
58
+ }, z.core.$strip>, z.ZodObject<{
59
+ type: z.ZodLiteral<"docker-ssh">;
60
+ ssh: z.ZodObject<{
61
+ host: z.ZodString;
62
+ port: z.ZodDefault<z.ZodNumber>;
63
+ username: z.ZodString;
64
+ identityFile: z.ZodOptional<z.ZodString>;
65
+ knownHostsFile: z.ZodOptional<z.ZodString>;
66
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
67
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
68
+ }, z.core.$strip>;
69
+ remote: z.ZodDefault<z.ZodObject<{
70
+ baseDir: z.ZodDefault<z.ZodString>;
71
+ runtime: z.ZodDefault<z.ZodEnum<{
72
+ auto: "auto";
73
+ docker: "docker";
74
+ podman: "podman";
75
+ }>>;
76
+ }, z.core.$strip>>;
77
+ expose: z.ZodObject<{
78
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
79
+ containerPort: z.ZodDefault<z.ZodNumber>;
80
+ hostPort: z.ZodNumber;
81
+ }, z.core.$strip>;
16
82
  }, z.core.$strip>], "type">;
83
+ /** SSH 连接配置(parse 后) */
84
+ type SshConfig = z.infer<typeof SshConfigSchema>;
85
+ /** 端口暴露配置(parse 后) */
86
+ type PortExposeConfig = z.infer<typeof PortExposeSchema>;
87
+ /** docker-ssh 部署平台配置(parse 后) */
88
+ type DockerSshProviderConfig = z.infer<typeof DockerSshProviderConfigSchema>;
17
89
  /** 部署模块完整配置 Schema */
18
90
  declare const DeployConfigSchema: z.ZodObject<{
19
91
  provider: z.ZodDiscriminatedUnion<[z.ZodObject<{
20
92
  type: z.ZodLiteral<"vercel">;
21
93
  token: z.ZodString;
94
+ }, z.core.$strip>, z.ZodObject<{
95
+ type: z.ZodLiteral<"docker-ssh">;
96
+ ssh: z.ZodObject<{
97
+ host: z.ZodString;
98
+ port: z.ZodDefault<z.ZodNumber>;
99
+ username: z.ZodString;
100
+ identityFile: z.ZodOptional<z.ZodString>;
101
+ knownHostsFile: z.ZodOptional<z.ZodString>;
102
+ strictHostKeyChecking: z.ZodDefault<z.ZodBoolean>;
103
+ connectTimeoutSeconds: z.ZodDefault<z.ZodNumber>;
104
+ }, z.core.$strip>;
105
+ remote: z.ZodDefault<z.ZodObject<{
106
+ baseDir: z.ZodDefault<z.ZodString>;
107
+ runtime: z.ZodDefault<z.ZodEnum<{
108
+ auto: "auto";
109
+ docker: "docker";
110
+ podman: "podman";
111
+ }>>;
112
+ }, z.core.$strip>>;
113
+ expose: z.ZodObject<{
114
+ type: z.ZodDefault<z.ZodLiteral<"port">>;
115
+ containerPort: z.ZodDefault<z.ZodNumber>;
116
+ hostPort: z.ZodNumber;
117
+ }, z.core.$strip>;
22
118
  }, z.core.$strip>], "type">;
23
119
  services: z.ZodOptional<z.ZodObject<{
24
120
  db: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
@@ -49,6 +145,13 @@ declare const DeployConfigSchema: z.ZodObject<{
49
145
  accessKeySecret: z.ZodString;
50
146
  }, z.core.$strip>], "provisioner">>;
51
147
  }, z.core.$strip>>;
148
+ container: z.ZodOptional<z.ZodObject<{
149
+ runtime: z.ZodDefault<z.ZodEnum<{
150
+ auto: "auto";
151
+ docker: "docker";
152
+ podman: "podman";
153
+ }>>;
154
+ }, z.core.$strip>>;
52
155
  }, z.core.$strip>;
53
156
  /** 部署配置类型(parse 后) */
54
157
  type DeployConfig = z.infer<typeof DeployConfigSchema>;
@@ -70,6 +173,12 @@ declare const HaiDeployError: {
70
173
  readonly UNSUPPORTED_TYPE: _h_ai_core.HaiErrorDef;
71
174
  readonly CONFIG_ERROR: _h_ai_core.HaiErrorDef;
72
175
  readonly CREDENTIAL_ERROR: _h_ai_core.HaiErrorDef;
176
+ readonly CONTAINER_RUNTIME_NOT_FOUND: _h_ai_core.HaiErrorDef;
177
+ readonly CONTAINER_BUILD_FAILED: _h_ai_core.HaiErrorDef;
178
+ readonly REMOTE_CONNECT_FAILED: _h_ai_core.HaiErrorDef;
179
+ readonly REMOTE_COMMAND_FAILED: _h_ai_core.HaiErrorDef;
180
+ readonly IMAGE_TRANSFER_FAILED: _h_ai_core.HaiErrorDef;
181
+ readonly HEALTH_CHECK_FAILED: _h_ai_core.HaiErrorDef;
73
182
  };
74
183
  /**
75
184
  * 部署错误接口
@@ -94,7 +203,7 @@ interface DeployError {
94
203
  cause?: unknown;
95
204
  }
96
205
  /** 基础设施服务类型 */
97
- type ServiceType = 'db' | 'cache' | 'storage' | 'email' | 'sms';
206
+ type ServiceType = 'db' | 'cache' | 'storage' | 'email' | 'sms' | 'vecdb';
98
207
  /**
99
208
  * 应用扫描结果
100
209
  *
@@ -107,8 +216,12 @@ interface ScanResult {
107
216
  isSvelteKit: boolean;
108
217
  /** 目标平台 adapter 是否已安装(如 @sveltejs/adapter-vercel) */
109
218
  adapterInstalled: boolean;
219
+ /** 容器部署所需的 @sveltejs/adapter-node 是否已安装 */
220
+ nodeAdapterInstalled: boolean;
110
221
  /** 检测到的模块依赖需要的服务列表 */
111
222
  requiredServices: ServiceType[];
223
+ /** 各服务检测到的后端类型(如 db→postgresql/mysql,vecdb→qdrant),用于生成匹配的 sidecar */
224
+ serviceBackends: Partial<Record<ServiceType, string>>;
112
225
  /** 构建命令 */
113
226
  buildCommand: string;
114
227
  }
@@ -126,6 +239,10 @@ interface DeployResult {
126
239
  status: 'ready' | 'building' | 'error';
127
240
  /** 已设置的环境变量名列表 */
128
241
  envVarsSet: string[];
242
+ /** 部署 Provider 名称(如 vercel / docker-ssh),容器部署时返回 */
243
+ provider?: string;
244
+ /** 健康检查结果:passed 表示 HTTP 探测成功,container-only 表示仅确认容器运行 */
245
+ healthCheck?: 'passed' | 'container-only' | 'skipped';
129
246
  }
130
247
  /**
131
248
  * 基础设施开通结果
@@ -262,4 +379,4 @@ declare const deploy: DeployFunctions;
262
379
  /** 读取错误中的恢复清单;仅返回明确允许展示的字段。 */
263
380
  declare function getDeployRecovery(error: HaiError): DeployRecoveryInfo | undefined;
264
381
 
265
- export { type DeployAppOptions, type DeployConfig, type DeployConfigInput, DeployConfigSchema, type DeployCredentialOperations, type DeployError, type DeployFunctions, type DeployRecoveryInfo, type DeployResult, HaiDeployError, ProviderConfigSchema, type ProvisionResult, type ScanResult, type ServiceType, deploy, getDeployRecovery };
382
+ export { type DeployAppOptions, type DeployConfig, type DeployConfigInput, DeployConfigSchema, type DeployCredentialOperations, type DeployError, type DeployFunctions, type DeployRecoveryInfo, type DeployResult, type DockerSshProviderConfig, HaiDeployError, type PortExposeConfig, ProviderConfigSchema, type ProvisionResult, type ScanResult, type ServiceType, type SshConfig, deploy, getDeployRecovery };