@h-ai/deploy 1.0.0-beta.5 → 1.0.0-beta.6

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.en.md CHANGED
@@ -29,7 +29,17 @@ pnpm build --target desktop --platform windows --arch x64
29
29
 
30
30
  #### Build options
31
31
 
32
- `--target` selects compile (default), image, app or desktop. `--images serv|web|serv,web` selects images. `--runtime auto|podman|docker` selects the local engine; auto prefers reachable Podman. `--network` overrides build networking, `--no-http-proxy` disables inherited Podman proxies, and `--sqlite-prebuild-mirror` selects a trusted HTTP(S) binary mirror. Native `--platform` and `--arch` require the matching host SDK and Rust toolchain. Builds do not load deployment environments or runtime secrets.
32
+ `--target` selects compile (default), image, app or desktop. `--images serv|web|serv,web` selects images. `--runtime auto|podman|docker` selects the local engine; auto prefers reachable Podman. `--network` overrides build networking, `--no-http-proxy` disables this build proxy (same as --proxy-mode none), and `--sqlite-prebuild-mirror` selects a trusted HTTP(S) binary mirror. Native `--platform` and `--arch` require the matching host SDK and Rust toolchain. Builds do not load deployment environments or runtime secrets.
33
+
34
+ #### Download sources and proxies
35
+
36
+ `--image-source official|cn|custom` covers application base images and missing middleware images during packaging. `--npm-source` requires `--build` and controls Corepack/pnpm. The China preset uses 1ms for Docker Hub/GHCR and npmmirror for npm. First interactive choices are saved in `hai.json.build.sources`; flags override one invocation. Set `--proxy-mode inherit|system|custom|none`, `--proxy-url`, and `--proxy-bypass`; `--no-http-proxy` means none. Docker/remote Podman image pulls require engine-side proxy settings; build containers check connectivity separately. See [sources and proxies](../../docs/usage/configuration/delivery.md#下载源和代理) for custom sources, supported platforms and old-template updates.
37
+
38
+ SDK `DeployAppOptions.imageSource` accepts `{ type: 'cn' }` or `{ type: 'custom', registries: { 'docker.io': 'mirror.example.com/docker.io', 'ghcr.io': 'mirror.example.com/ghcr.io' } }`; `proxy` accepts `{ mode: 'inherit' }` or `{ mode: 'custom', url: 'http://127.0.0.1:10808', bypass: 'localhost' }`. Defaults use official sources and inherited process proxies. `resolveImageReference()` / `resolveDownloadProxy()` need no init and do not change engine settings. Middleware archives preserve canonical references for offline server imports.
39
+
40
+ ```sh
41
+ pnpm run deploy --env prod --build --bundle-only --image-source cn --npm-source cn
42
+ ```
33
43
 
34
44
  #### Describe packaging in hai.json
35
45
 
@@ -101,10 +111,9 @@ Prepare `.env.<environment>` and the project root `hai.json`, with image/native
101
111
 
102
112
  The deployment SDK uses these priorities, highest first:
103
113
 
104
- - `HAI_DEPLOY_*`: process > `.env.<environment>.local` > `.env.<environment>` > legacy `.env.local` > `.env`.
105
- - Other modules: process > `.env.<environment>.local` > legacy `.env.local` > `.env.<environment>` > `.env`, preserving existing private credentials.
114
+ All `HAI_*` settings use process > `.env.<mode>.local` > `.env.local` > `.env.<mode>` > `.env` > YAML. Within one source, fields override base JSON. Replacing a whole node from a higher source clears lower-source fields. Node/Vite and deployment share Core resolution.
106
115
 
107
- New connection settings and middleware secrets go to the Git-ignored `.env.<environment>.local`; public backends and addresses stay in `.env.<environment>`. Legacy `.env.local` remains a shared fallback and is never migrated or deleted automatically. Explicit deployment targets for the selected environment take precedence. Only the deployment SDK loads the environment-specific private file; ordinary Core application loading is unchanged. Builds do not load deployment environments or secrets.
116
+ Public settings live in `.env` and `.env.<mode>`; SSH settings and credentials go to Git-ignored `.env.<mode>.local`. `.env.local` contains shared machine-private settings. Builds do not read deployment secrets.
108
117
 
109
118
  For existing projects, move the previous build object into `hai.json.build`, convert deployment YAML into `hai.json.deploy`, remove the old files and retain `HAI_DEPLOY_*` variables. Existing self-hosted S3 installations must use a full deployment for this upgrade: their old baseline contains an exited initialization service, which fails the dependency checks of new incremental packages. The full deployment also refreshes the `createBucket` middleware setting; subsequent incremental updates work normally.
110
119
 
@@ -118,7 +127,7 @@ SSH settings are saved in `.env.<environment>.local`. `--bundle-only` offers to
118
127
 
119
128
  Runtime lockfile dependencies and configuration determine middleware. Choose defaults, custom connections or keep settings. With complete target-environment settings, `--yes` preserves values without asking again or provisioning cloud resources again; interactive mode still lets you select the scope. --skip-middleware skips configuration/provisioning. SQLite, memory cache and local storage need no external images. Complete bundles automatically include self-hosted middleware images. External services still require reachable destinations; cloud provisioning is not an offline operation.
120
129
 
121
- Default and custom modes preserve effective settings for the same backend, including pool, operation log, public storage URLs/prefixes, Redis retry and vector search options absent from the form. Managed services replace connection selectors and credentials; old URLs, TLS/cluster modes and credentials cannot override the new service. SMTP senders and LDAP attribute mappings remain. Switching backends clears incompatible fields. Missing fields already saved in parent JSON need recovery from the original configuration or backup: restore `publicEndpoint` in the highest-priority `HAI_STORAGE`, then rebuild and deploy the bundle. Editing YAML or leaf variables hidden by that parent JSON cannot repair an old bundle.
130
+ Default and custom modes preserve effective settings for the same backend, including pool, operation log, public storage URLs/prefixes, Redis retry and vector search options absent from the form. Managed services replace connection selectors and credentials; old URLs, TLS/cluster modes and credentials cannot override the new service. SMTP senders and LDAP attribute mappings remain. Switching backends clears incompatible fields. Missing fields already saved in parent JSON need recovery from the original configuration or backup: restore `publicEndpoint` in the highest-priority `HAI_STORAGE`, then rebuild and deploy the bundle. Editing YAML or leaf variables hidden by that parent JSON cannot repair an old bundle. Generators prefer field variables and retain JSON arrays. Handwritten JSON remains a base and appears before its module fields; fields in the same source win. Dynamic maps, JSON text and explicit empty objects preserve their meaning. Expanding a whole-object replacement retains its highest `{}` reset marker so old YAML fields cannot reappear. Container paths stay in delivery output without changing source settings.
122
131
 
123
132
  #### Direct private object storage access
124
133
 
@@ -241,11 +250,17 @@ SSH interruptions initially report `remoteExecution: 'unknown'`. The docker-ssh
241
250
 
242
251
  Environment flags, initial account passwords and the transport key ring are also read directly by startup code. They remain available even when a similarly named configuration parent exists.
243
252
 
244
- Delivery removes every HAI child variable hidden by a parent and omits settings supplied by `middleware.env` from `.env.runtime`. Container paths stay in delivery files; pruning leaves the source environment intact. Edit the retained parent object or managed connection file.
253
+ Generators prefer field variables and retain JSON arrays. Handwritten JSON remains a base and appears before its module fields; fields in the same source win. Dynamic maps, JSON text and explicit empty objects preserve their meaning. Expanding a whole-object replacement retains its highest `{}` reset marker so old YAML fields cannot reappear. Container paths stay in delivery output without changing source settings.
254
+
255
+ The deployment archive has five top-level entries: `apply.sh`, `manifest.json`, `config/`, `images/`, and `.hai/`. `config/.env.runtime` is the single application environment file. `manifest.json.metadata` holds nonsecret layout, dependencies, health paths, image lists and managed bindings. `.hai/compose.json` is a complete snapshot; partial updates replace selected service definitions and retain other services without historical Compose chains. `.hai/` also contains native environment, the loader, Nginx/TLS, policy and checksums. Secrets stay out of the manifest; helper scripts are embedded in `apply.sh`. Failure recovery restores application configuration before services while protecting credentials, backend ledgers and volumes. Bundle format 2 has no old-layout compatibility or migration. Runtime preparation reuses the already imported Node application image in a temporary container with networking disabled. It downloads no extra image and requires no host Node or jq.
256
+
257
+ Node backends (Serv / SvelteKit SSR) read `<deployment-root>/config/.env.runtime` on every start. This single application file includes managed connections. Edit a field such as `HAI_STORAGE_PUBLICENDPOINT`, then restart the backend; fields override parent JSON within the same source. The configuration directory is mounted read-only at `/run/hai/config`, so atomic file replacement is visible on restart. Private proxy identity and Node startup options remain deployment settings. Static Web, native middleware environment, ports/mounts, and Nginx routes or limits require deployment. Web/middleware-only updates retain the file; partial backend updates preserve managed connections, while full updates use the new package with existing managed credentials. Back up edits and copy permanent settings into the source environment.
258
+
259
+ Container packaging sets the listener to `0.0.0.0` and the port declared by the artifact, falling back to deployment settings when absent. It generates `HOST`, `PORT`, and the matching Serv fields. Loopback addresses and development ports in local `.env` files remain local settings. Update the artifact and deployment layout together when changing container ports.
245
260
 
246
- Node backends (Serv / SvelteKit SSR) reread the release `.env.runtime` on every start by default. Edit it, then run `docker restart <container>` or `podman restart <container>`; no image rebuild or extra override is needed. Existing deployments need one deployment with the new CLI. Removed settings do not survive through stale Compose injection. Managed connections in `middleware.env` retain precedence; private proxy identity and Node startup options remain deployment settings. Static Web, middleware, ports/mounts, and Nginx routes and limits keep their existing update flow. Gateway path, port or mount changes require a full deployment. Back up manual edits and copy permanent changes into the source environment; the next backend deployment regenerates the file.
261
+ Nginx dynamically tracks DNS changes for application and storage containers. The shared `.hai/proxy-start.sh` runs as an image entrypoint hook, preserves the image startup command, and reads resolvers from the container's `/etc/resolv.conf` and also watches certificate changes for HTTPS. Docker and Podman need no fixed DNS address. After editing runtime configuration and restarting the backend, the proxy reconnects automatically even if its IP changes.
247
262
 
248
- Persisted framework configuration uses `HAI_*` names. Native `POSTGRES_*`, `MYSQL_*`, `RUSTFS_*`, `MINIO_*`, and `LDAP_*` variables keep the names required by middleware images, but are generated only in private `middleware-runtime.env`, loaded by middleware containers. They are no longer saved to project environment files or the application's `.env.runtime`. The `middleware.env` retained for incremental application updates contains framework `HAI_*` settings only. Previously saved native entries are not deleted automatically and are excluded from new application environments. Existing services keep their original release files when a new bundle is deployed incrementally.
263
+ Image-native `POSTGRES_*`, `MYSQL_*`, `RUSTFS_*`, `MINIO_*`, and `LDAP_*` keep the names required by their images, only in private `.hai/middleware-runtime.env` loaded by middleware. All application connections are merged into `config/.env.runtime`.
249
264
 
250
265
  Whole `HAI_*` configuration supplied by sidecars or provisioners clears superseded leaf variables: Redis overrides no longer leave `HAI_CACHE_TYPE=memory`. Global `~/.hai/credentials.yml` fills missing settings only, respecting existing environment/process keys and parent objects. Deployment starts selected middleware and waits for PostgreSQL/MySQL/Redis/S3 health before serv/Web; incremental deployment and recovery use the same order and preserve unselected containers.
251
266
 
@@ -253,9 +268,9 @@ On Podman, deployment actively runs each configured container healthcheck and re
253
268
 
254
269
  Only the first startup batch removes orphan containers; later serv/Web startup preserves the middleware that is already ready.
255
270
 
256
- Generated fullstack projects create different administrator passwords and transport key rings in `.env.dev/.env.test/.env.prod`. Commit these environment files to share them after cloning; local private files and process variables can override them. Before upload, deployment identifies missing or malformed `HAI_CRYPTO_TRANSPORT_SERVERKEYRING` for transport-enabled applications. All nodes in an environment use the same valid ring from its environment file or a Secret Manager override; never generate one per node. CLI completion identifies the actual source of `HAI_ADMIN_DEFAULT_PASSWORD` without printing it, independently of log level. It applies only when initially creating the administrator. Offline scripts also identify the password variable in the server's private `.env.runtime`.
271
+ Project creation generates independent initial passwords and transport key rings in `.env.dev.local`, `.env.test.local`, and `.env.prod.local`. These files are Git-ignored. Distribute environment secrets through protected files or a Secret Manager after cloning. Nodes in one environment share a valid ring and registry; never generate a ring at startup. Missing or invalid `HAI_CRYPTO_TRANSPORT_SERVERKEYRING` fails before upload. CLI and offline instructions identify password variables and sources without showing values. Initial passwords create missing accounts and do not reset existing accounts.
257
272
 
258
- Each delivery still generates `.hai/deploy/<environment>/bundle-.../.env.runtime` for Compose. This is generated output, not the saved configuration source. The default `NODE_ENV` follows the target: `test` → `test`, `dev/development` → `development`, and `prod/production` or other modes → `production`. Explicit `NODE_ENV` values in environment configuration override this default; build optimization does not select the runtime environment. Rebuild the bundle to update old deliveries.
273
+ Each delivery still generates `config/.env.runtime` inside the archive for Compose. This is generated output, not the saved configuration source. The default `NODE_ENV` follows the target: `test` → `test`, `dev/development` → `development`, and `prod/production` or other modes → `production`. Explicit `NODE_ENV` values in environment configuration override this default; build optimization does not select the runtime environment. Rebuild the bundle to update old deliveries.
259
274
 
260
275
  Project-root `hai.json.deploy` stores deployment structure, `.env.<environment>` stores public settings, and `.env.<environment>.local` stores SSH settings and middleware secrets. Environment files default to the project/workspace root; override with `--env-dir`. After configuration, `pnpm run deploy --env test --build --bundle-only --yes` reuses complete settings without prompts; add `--skip-middleware` to retain middleware configuration. Interactive bundle-only mode offers to save or update SSH settings at the final configuration step; editing a delivery's `deploy-target.json` does not update the project.
261
276
 
package/README.ja.md CHANGED
@@ -29,7 +29,17 @@ pnpm build --target desktop --platform windows --arch x64
29
29
 
30
30
  #### build のオプション
31
31
 
32
- `--target` は compile(既定)、image、app、desktop を選択します。`--images serv|web|serv,web` は対象イメージを、`--runtime auto|podman|docker` はローカルエンジンを選択します。auto は接続可能な Podman を優先します。`--network` は構築ネットワーク、`--no-http-proxy` は Podman のプロキシ継承無効、`--sqlite-prebuild-mirror` は信頼できる HTTP(S) バイナリ配布元です。ネイティブ配布物には対象 OS の SDK と Rust ツールチェーンが必要です。構築時にデプロイ環境や秘密情報を読み込みません。
32
+ `--target` は compile(既定)、image、app、desktop を選択します。`--images serv|web|serv,web` は対象イメージを、`--runtime auto|podman|docker` はローカルエンジンを選択します。auto は接続可能な Podman を優先します。`--network` は構築ネットワーク、`--no-http-proxy` は 今回のビルドのプロキシ無効(none と同じ)、`--sqlite-prebuild-mirror` は信頼できる HTTP(S) バイナリ配布元です。ネイティブ配布物には対象 OS の SDK と Rust ツールチェーンが必要です。構築時にデプロイ環境や秘密情報を読み込みません。
33
+
34
+ #### 取得元とプロキシ
35
+
36
+ `--image-source official|cn|custom` はアプリのベースと梱包時に不足したミドルウェアイメージに適用します。`--npm-source` は `--build` と併用し Corepack/pnpm を制御します。中国本土は毫秒镜像の Docker Hub/GHCR 通道と npmmirror を使用します。初回対話で `hai.json.build.sources` に保存し、引数は今回のみ上書きします。プロキシは `--proxy-mode inherit|system|custom|none`、`--proxy-url`、`--proxy-bypass` で指定し、`--no-http-proxy` は none と同じです。Docker/リモート Podman の取得はエンジン側の設定が必要で、ビルドコンテナの接続は別途確認します。カスタム設定、対応 OS、旧テンプレートの更新は[取得元とプロキシ](../../docs/usage/configuration/delivery.md#下载源和代理)を参照してください。
37
+
38
+ SDK の `DeployAppOptions.imageSource` は `{ type: 'cn' }` または `{ type: 'custom', registries: { 'docker.io': 'mirror.example.com/docker.io', 'ghcr.io': 'mirror.example.com/ghcr.io' } }`、`proxy` は `{ mode: 'inherit' }` または `{ mode: 'custom', url: 'http://127.0.0.1:10808', bypass: 'localhost' }` です。既定は公式とプロセスの継承です。`resolveImageReference()`/`resolveDownloadProxy()` は init 不要でエンジン設定を変更しません。ミドルウェアの元参照をアーカイブに保ち、サーバーではオフラインで読み込みます。
39
+
40
+ ```sh
41
+ pnpm run deploy --env prod --build --bundle-only --image-source cn --npm-source cn
42
+ ```
33
43
 
34
44
  #### hai.json でパッケージの構成を指定する
35
45
 
@@ -101,10 +111,9 @@ CLI の --project-name と SDK の DeployAppOptions.projectName 上書きは廃
101
111
 
102
112
  デプロイ SDK の優先順は次のとおりです(高い順)。
103
113
 
104
- - `HAI_DEPLOY_*`:プロセス > `.env.<環境>.local` > `.env.<環境>` > 旧 `.env.local` > `.env`。
105
- - その他のモジュール:プロセス > `.env.<環境>.local` > 旧 `.env.local` > `.env.<環境>` > `.env`。既存の非公開認証情報の優先順を維持します。
114
+ すべての `HAI_*` はプロセス > `.env.<mode>.local` > `.env.local` > `.env.<mode>` > `.env` > YAML の順です。同一ソースは JSON を基礎として個別フィールドで上書きし、高優先度ソースの全体置換は低優先度の細項を解除します。Node/Vite と配備は同じ Core 処理を使います。
106
115
 
107
- 新しい接続情報とミドルウェアの秘密値は Git 対象外の `.env.<環境>.local` に、公開バックエンドとアドレスは `.env.<環境>` に保存します。旧 `.env.local` は共通の互換フォールバックとして残し、自動移行・削除しません。選択環境で明示したデプロイ先が優先されます。環境別の非公開ファイルを読むのはデプロイ SDK のみで、Core の通常のアプリ読み込み規則は変わりません。構築時にはデプロイ環境や秘密値を読み込みません。
116
+ 公開設定は `.env` と `.env.<mode>`、SSH と資格情報は Git 対象外の `.env.<mode>.local`、マシン共通の秘密値は `.env.local` に保存します。構築時には配備の秘密値を読みません。
108
117
 
109
118
  既存プロジェクトは旧構築オブジェクトを `hai.json.build`、旧配置 YAML を JSON に変換して `hai.json.deploy` に移し、元のファイルを削除します。`HAI_DEPLOY_*` は継続利用できます。既存の自前 S3 は今回の初回更新で全量配置が必要です。旧基準には終了済みの初期化サービスが残っており、新しい増分パッケージの依存稼働チェックで拒否されます。全量配置で `createBucket` 設定も更新した後は増分更新を継続できます。
110
119
 
@@ -116,7 +125,7 @@ SSH 設定は選択環境の `.env.<環境>.local` に保存します。`--bundl
116
125
 
117
126
  実行依存と設定に基づき、既定、自分の接続、設定維持を選びます。対象環境の設定が完全なら、`--yes` は値を保持し、再入力やクラウド資源の再作成を行いません。対話モードでは今回の処理範囲を選べます。--skip-middleware は設定と開通を省略します。SQLite、メモリーキャッシュ、ローカル保存には外部イメージがありません。完全配布物には自前ミドルウェアのイメージも自動で含めます。外部サービスは接続先への到達性が必要で、クラウド開通はオフライン処理ではありません。
118
127
 
119
- 既定とカスタムは同じバックエンドの有効な設定を保持します。フォームにない接続プール、操作ログ、ストレージ公開 URL/接頭辞、Redis 再試行、ベクトル検索設定も対象です。自前構成とクラウド開通は接続先・認証だけを更新し、古い URL、TLS/クラスター設定、認証情報を引き継ぎません。SMTP 送信元と LDAP 属性対応は保持します。バックエンド変更時は非互換項目を除去します。旧版が親 JSON から削除した項目は自動復元できません。元の設定やバックアップから最優先の `HAI_STORAGE` に `publicEndpoint` などを戻して配布物を再作成してください。親 JSON に隠された YAML や末端変数だけの変更では直りません。
128
+ 既定とカスタムは同じバックエンドの有効な設定を保持します。フォームにない接続プール、操作ログ、ストレージ公開 URL/接頭辞、Redis 再試行、ベクトル検索設定も対象です。自前構成とクラウド開通は接続先・認証だけを更新し、古い URL、TLS/クラスター設定、認証情報を引き継ぎません。SMTP 送信元と LDAP 属性対応は保持します。バックエンド変更時は非互換項目を除去します。 生成処理は個別変数を優先し、配列は JSON を維持します。手書き JSON はモジュール細項の前に置く基礎値で、同一ソースの細項が優先されます。動的辞書、JSON テキスト、明示的な空オブジェクトの意味を維持します。全体置換を展開する際は最上位の `{}` リセットを残し、古い YAML フィールドの復活を防ぎます。コンテナー内パスは配布物のみ変更します。
120
129
 
121
130
  #### 非公開オブジェクトストレージへの直接アクセス
122
131
 
@@ -239,11 +248,17 @@ SSH 中断は最初に `remoteExecution: 'unknown'` とし、docker-ssh SDK は
239
248
 
240
249
  環境フラグ・初期アカウントパスワード・転送キーリングは起動コードも直接読み取るため、同じ接頭辞の親設定があっても保持します。
241
250
 
242
- 配布時には HAI 親変数で隠れるすべての子項目と、`middleware.env` が提供する `.env.runtime` の重複項目を除去します。コンテナ内パスは配布設定に保存し、この除去処理で元の環境は変更しません。残った親オブジェクトまたは管理接続ファイルを編集します。
251
+ 生成処理は個別変数を優先し、配列は JSON を維持します。手書き JSON はモジュール細項の前に置く基礎値で、同一ソースの細項が優先されます。動的辞書、JSON テキスト、明示的な空オブジェクトの意味を維持します。全体置換を展開する際は最上位の `{}` リセットを残し、古い YAML フィールドの復活を防ぎます。コンテナー内パスは配布物のみ変更します。
252
+
253
+ 配布アーカイブのトップレベルは `apply.sh`、`manifest.json`、`config/`、`images/`、`.hai/` の 5 項目です。アプリ環境は `config/.env.runtime` に統一し、レイアウト、依存、健康パス、イメージ一覧、管理接続などの非機密メタデータは `manifest.json.metadata` に保存します。`.hai/compose.json` は全サービスのスナップショットで、部分更新は選択サービスの定義を置換し、他サービスを保持します。`.hai/` にネイティブ環境、ローダー、Nginx/TLS、ポリシー、チェックサムも保存し、秘密値は manifest に含めません。補助スクリプトは `apply.sh` に統合します。失敗時は先にアプリ設定、次にサービスを復元し、資格情報、台帳、データボリュームを保護します。形式 2 の旧ディレクトリー互換や移行は実装しません。 設定の確定は導入済みの Node アプリイメージを一時コンテナーで再利用し、ネットワークを無効化します。追加イメージの取得やホストの Node・jq は不要です。
254
+
255
+ Node バックエンド(Serv / SvelteKit SSR)は起動ごとに `<配備ディレクトリー>/config/.env.runtime` を読みます。管理接続も同じアプリ設定に統合します。`HAI_STORAGE_PUBLICENDPOINT` などを編集して再起動すると反映され、同じソースでは個別フィールドが親 JSON を上書きします。設定ディレクトリーを `/run/hai/config` に読み取り専用でマウントするため、ファイルのアトミック置換も次の起動で反映されます。非公開プロキシ識別と Node 起動引数は配備設定です。静的 Web、ネイティブ環境、ポート、マウント、Nginx の変更は再配備します。Web/ミドルウェアのみの更新は設定を維持し、バックエンドの部分更新は管理接続を維持、全量更新は新しい設定と既存の管理資格情報を使います。編集前にバックアップし、恒久的な値は元の環境にも反映してください。
256
+
257
+ コンテナーのパッケージ化では、待受アドレスを `0.0.0.0`、ポートを成果物の宣言値(未指定なら配置設定)として、`HOST`、`PORT` と Serv の対応フィールドを生成します。ローカル `.env` のループバックアドレスと開発ポートはローカル実行に使います。コンテナーポートの変更時は成果物と配置構成を更新して再配備してください。
243
258
 
244
- Node バックエンド(Serv / SvelteKit SSR)は起動ごとに配布先の `.env.runtime` を既定で読み直します。編集後に `docker restart <コンテナー>` または `podman restart <コンテナー>` を実行し、イメージ再構築や追加 override は不要です。既存配置は新 CLI で一度デプロイしてください。削除した設定を旧 Compose 環境から復活させず、管理接続の `middleware.env` を優先します。私有プロキシ識別と Node 起動引数は配置設定のままです。静的 Web・ミドルウェア・ポート/マウント・Nginx の経路と制限は従来手順で更新します。ゲートウェイの経路、ポート、マウントの変更は全量デプロイを行います。手動変更をバックアップし、継続する値は元の環境にも反映してください。次のバックエンド配置時に再生成されます。
259
+ Nginx はアプリとストレージのコンテナーの DNS 変更に動的に追従します。共通の `.hai/proxy-start.sh` はイメージの元の起動コマンドを維持する入口フックとして動作し、コンテナー内の `/etc/resolv.conf` からリゾルバーを読み、HTTPS では証明書更新も監視します。Docker と Podman の DNS アドレスを固定する必要はありません。実行時設定を編集してバックエンドを再起動した後、IP が変わってもプロキシは自動的に再接続します。
245
260
 
246
- フレームワークの永続設定は `HAI_*` を使います。`POSTGRES_*`、`MYSQL_*`、`RUSTFS_*`、`MINIO_*`、`LDAP_*` はミドルウェアイメージ必須の名前を維持し、非公開の `middleware-runtime.env` にだけ生成してミドルウェアに渡します。プロジェクト環境ファイルやアプリの `.env.runtime` には新規保存しません。増分アプリ更新用の `middleware.env` は `HAI_*` 設定のみです。既存の原生変数は自動削除せず、新しいアプリ環境から除外します。未更新サービスは以前の配布ファイルを使い続けます。
261
+ イメージ固有の `POSTGRES_*`、`MYSQL_*`、`RUSTFS_*`、`MINIO_*`、`LDAP_*` は必要な名前を維持し、ミドルウェア専用の非公開 `.hai/middleware-runtime.env` に保存します。アプリ接続はすべて `config/.env.runtime` に統合します。
247
262
 
248
263
  サイドカーやクラウドの `HAI_*` オブジェクトで置換した古い子変数は除去し、Redis 選択後に `HAI_CACHE_TYPE=memory` を残しません。グローバル `~/.hai/credentials.yml` は不足値のみ補い、環境・プロセスの同名キーや親設定を上書きしません。選択したミドルウェアを起動し PostgreSQL/MySQL/Redis/S3 の正常性を確認後、serv/Web を起動します。増分と復旧も同じ順序で、未選択のコンテナーは維持します。
249
264
 
@@ -251,9 +266,9 @@ Podman では設定済みの healthcheck を明示的に実行して状態を再
251
266
 
252
267
  孤立コンテナーの削除は最初の起動バッチのみで行い、後続の serv/Web 起動では準備済みのミドルウェアを維持します。
253
268
 
254
- 作成時に `.env.dev/.env.test/.env.prod` ごとに別の初期管理者パスワードと転送鍵リングを生成し、Git に保存します。クローン後も環境ごとに共有し、ローカル秘密ファイルやプロセス環境で上書きできます。transport 有効アプリの `HAI_CRYPTO_TRANSPORT_SERVERKEYRING` が不足・不正ならアップロード前に設定元を案内します。同じ環境の全ノードは環境ファイルまたは Secret Manager の上書きから同じ有効リングを読み込み、ノードごとに生成しないでください。CLI 完了時はログレベルによらず `HAI_ADMIN_DEFAULT_PASSWORD` の実際の保存元を案内し、値は表示しません。初回管理者作成時だけ有効です。 オフライン入口でも、サーバーの非公開 `.env.runtime` 内のパスワード変数の場所を案内します。
269
+ 作成時に `.env.dev.local`、`.env.test.local`、`.env.prod.local` に個別の初期パスワードと転送鍵リングを生成し、Git には登録しません。クローン後は保護ファイルまたは Secret Manager で環境の秘密値を配布します。同じ環境のノードは有効な ring と共有 registry を使い、起動時に生成しません。`HAI_CRYPTO_TRANSPORT_SERVERKEYRING` の不足・不正はアップロード前に失敗します。CLI とオフライン説明はパスワード変数と保存元のみ示し、値は表示しません。初期値は不足アカウント作成時のみ使用し、既存アカウントを変更しません。
255
270
 
256
- 配布ごとに起動時読み込み用の `.hai/deploy/<環境>/bundle-.../.env.runtime` を生成します。これは生成物であり、設定の保存元ではありません。`NODE_ENV` の既定値は対象環境に従い、`test` は `test`、`dev/development` は `development`、`prod/production` とその他は `production` です。環境設定の明示的な `NODE_ENV` が優先され、構築の最適化モードとは別です。旧配布物は再度パッケージ化してください。
271
+ 配布ごとに起動時読み込み用の アーカイブ内の `config/.env.runtime` を生成します。これは生成物であり、設定の保存元ではありません。`NODE_ENV` の既定値は対象環境に従い、`test` は `test`、`dev/development` は `development`、`prod/production` とその他は `production` です。環境設定の明示的な `NODE_ENV` が優先され、構築の最適化モードとは別です。旧配布物は再度パッケージ化してください。
257
272
 
258
273
  プロジェクトルートの `hai.json.deploy` に配置構造、`.env.<環境>` に公開設定、`.env.<環境>.local` に SSH とミドルウェアの認証情報を保存します。環境ファイルは既定でプロジェクト/workspace ルート、変更時は `--env-dir` を指定します。設定後は `pnpm run deploy --env test --build --bundle-only --yes` で再入力を省略でき、ミドルウェア設定を保持する場合は `--skip-middleware` を追加します。対話式 bundle-only は最後に SSH 設定の保存・変更を確認します。配布物の `deploy-target.json` の編集はプロジェクトに反映されません。
259
274
 
package/README.md CHANGED
@@ -37,10 +37,20 @@ pnpm build --target desktop --platform windows --arch x64
37
37
  | `--images` | serv、web 或 serv,web;只构建导出所选镜像 |
38
38
  | `--runtime` | auto(优先 Podman)、podman、docker |
39
39
  | `--network` | 容器构建网络,默认引擎设置 |
40
- | `--no-http-proxy` | Podman 构建不继承本机代理 |
40
+ | `--no-http-proxy` | 禁用本次构建代理(等价 none) |
41
41
  | `--sqlite-prebuild-mirror` | SQLite 预编译包的可信 HTTP(S) 根地址 |
42
42
  | `--platform` / `--arch` | 原生安装包的系统与架构 |
43
43
 
44
+ #### 下载源和代理
45
+
46
+ `--image-source official|cn|custom` 同时作用于应用基础镜像和打包时缺少的中间件镜像。`--npm-source` 随 `--build` 控制 Corepack/pnpm;cn 使用毫秒镜像的 Docker Hub/GHCR 通道和 npmmirror。首次交互保存 `hai.json.build.sources`,命令覆盖只作用本次。代理使用 `--proxy-mode inherit|system|custom|none`、`--proxy-url` 与 `--proxy-bypass`;`--no-http-proxy` 等价 none。Docker/远程 Podman 的镜像拉取代理须在引擎侧设置,构建容器另做可达性检查。自定义源、平台范围及旧模板迁移见[下载源和代理](../../docs/usage/configuration/delivery.md#下载源和代理)。
47
+
48
+ SDK 的 `DeployAppOptions.imageSource` 为 `{ type: 'cn' }` 或 `{ type: 'custom', registries: { 'docker.io': 'mirror.example.com/docker.io', 'ghcr.io': 'mirror.example.com/ghcr.io' } }`,`proxy` 为 `{ mode: 'inherit' }` 或 `{ mode: 'custom', url: 'http://127.0.0.1:10808', bypass: 'localhost' }`。缺省为官方源、继承进程代理。`resolveImageReference()` / `resolveDownloadProxy()` 是无需 init 的解析工具,不修改引擎设置。中间件归档保留原始引用,服务器继续离线导入。
49
+
50
+ ```sh
51
+ pnpm run deploy --env prod --build --bundle-only --image-source cn --npm-source cn
52
+ ```
53
+
44
54
  #### 用 hai.json 描述要打什么包
45
55
 
46
56
  ```json
@@ -114,10 +124,9 @@ CLI 的 --project-name 与 SDK 的 DeployAppOptions.projectName 覆盖入口已
114
124
 
115
125
  部署 SDK 的环境优先级从高到低为:
116
126
 
117
- - `HAI_DEPLOY_*`:进程 > `.env.<环境>.local` > `.env.<环境>` > 旧 `.env.local` > `.env`。
118
- - 其他模块:进程 > `.env.<环境>.local` > 旧 `.env.local` > `.env.<环境>` > `.env`,保留原有私有凭据优先级。
127
+ 所有 `HAI_*` 统一使用:进程 > `.env.<mode>.local` > `.env.local` > `.env.<mode>` > `.env` > YAML。同来源 JSON 为基础,细项继续覆盖;高来源整段替换先清掉低来源细项。Node/Vite 与部署共用 Core 合并,不维护模块专用顺序。
119
128
 
120
- 新保存的连接和中间件凭据写入忽略 Git 的 `.env.<环境>.local`;公共后端和地址保留在 `.env.<环境>`。旧 `.env.local` 继续作为共享兼容回退,不自动迁移或删除;所选环境明确的部署目标优先。按环境私有文件仅由部署 SDK 加载,Core 的普通应用加载规则不变。构建不加载部署环境和密钥。
129
+ 公共配置保存在 `.env` 和 `.env.<mode>`,连接凭据与 SSH 配置保存在忽略 Git 的 `.env.<mode>.local`,`.env.local` 用于本机共享私有值。构建不读取部署秘密。
121
130
 
122
131
  已有项目需把原构建对象移入 `hai.json.build`,把原部署 YAML 转成 JSON 放入 `hai.json.deploy`,删除原文件并保留现有 `HAI_DEPLOY_*` 变量。已有自建 S3 的项目首次升级必须全量部署:旧成功基线仍含已退出的一次性初始化服务,直接使用新增量包会被依赖存活检查拒绝;全量部署同时刷新中间件环境中的 `createBucket`,之后即可继续增量更新。
123
132
 
@@ -129,7 +138,7 @@ SSH 参数保存到选中环境的 `.env.<环境>.local`。`--bundle-only` 在
129
138
 
130
139
  根据锁文件运行依赖和模块配置识别数据库、缓存、文件存储、向量库、LDAP、邮件等。逐项选择默认、自定义或保持。目标环境已有完整配置时,`--yes` 保持原值,不重新输入或重复云开通;交互模式仍可选择本次处理范围。配置完备可用 `--skip-middleware` 跳过向导和开通。SQLite、Memory cache、本地存储不产生外部镜像;Compose 自建服务的镜像自动加入完整交付包。外部服务仍需目标网络可达,云开通不属于完全离线部署。
131
140
 
132
- 默认和自定义模式均保留同后端的有效配置,包括表单未展示的连接池、操作日志、存储公开端点/前缀、Redis 重试和向量检索选项。自建与云开通仅重新定稿实际连接和认证;旧 URL、TLS/集群模式与凭据不能覆盖新服务,SMTP 发件人和 LDAP 属性映射继续保留。切换后端时清除不兼容字段。旧向导已保存的缺字段父 JSON 不会自动恢复:须从原配置或备份补回最高优先级 `HAI_STORAGE` 中的 `publicEndpoint` 等字段,再重新打包部署;只改被父 JSON 覆盖的 YAML/叶变量无效。
141
+ 默认和自定义模式均保留同后端的有效配置,包括表单未展示的连接池、操作日志、存储公开端点/前缀、Redis 重试和向量检索选项。自建与云开通仅重新定稿实际连接和认证;旧 URL、TLS/集群模式与凭据不能覆盖新服务,SMTP 发件人和 LDAP 属性映射继续保留。切换后端时清除不兼容字段。 生成端优先写细项,数组保留 JSON;手写 JSON 允许兜底,放在对应模块细项之前。同源细项覆盖 JSON,动态字典、JSON 文本和显式空对象保留原义;整体替换展开时以最上层 `{}` 标记阻止旧 YAML 字段恢复。容器路径只写交付配置,不反写工程源环境。
133
142
 
134
143
  #### 私有对象存储直连
135
144
 
@@ -263,13 +272,19 @@ SSH 中断始终先记 `remoteExecution: 'unknown'`。docker-ssh SDK 用新连
263
272
 
264
273
  ### 运行环境文件与重复部署
265
274
 
266
- Node 后端(Serv / SvelteKit SSR)默认在每次启动时重读发布目录的 `.env.runtime`,修改后 `docker restart <容器名>` 或 `podman restart <容器名>` 即可,无需重建镜像或额外 override。旧部署先用新版 CLI 发布一次。删除的字段不会从旧 Compose 注入中复活;服务器托管连接的 `middleware.env` 保持原有更高优先级,私有代理标记和 Node 启动参数仍由部署装配管理。静态 Web、中间件、端口/挂载、Nginx 路由与限额不在该重载范围;改变网关路径、端口或挂载时使用全量部署。手工修改需备份;下一次后端发布会从工程环境重新生成,需要长期保留的配置同步回源环境。
275
+ Node 后端(Serv / SvelteKit SSR)每次启动读取服务器 `<部署目录>/config/.env.runtime`。应用配置与托管连接在此合并,修改 `HAI_STORAGE_PUBLICENDPOINT` 等细项后重启对应后端即可;同一来源的细项覆盖父 JSON。配置目录只读挂载到 `/run/hai/config`,编辑器原子替换文件也会被下一次启动读取。私有代理标记和 Node 启动参数由部署装配管理;静态 Web、中间件原生环境、端口/挂载、Nginx 路由与限额变更仍需部署。仅更新 Web/中间件保留当前应用配置;后端局部更新保留服务器托管连接,其余值来自新包,全量更新采用新包并按已有凭据状态定稿。手工修改先备份,长期配置同步回源环境。
276
+
277
+ 容器打包时,监听地址采用 `0.0.0.0`,端口采用制品声明值(未声明时采用部署配置),并生成 `HOST`、`PORT` 和 Serv 的对应细项。本机 `.env` 中的回环地址与开发端口继续用于本地运行。修改容器端口时更新制品与部署布局后重新部署。
278
+
279
+ Nginx 通过动态上游跟随应用和存储容器的 DNS 地址变化。共用的 `.hai/proxy-start.sh` 作为镜像入口钩子执行,保留镜像原有启动命令,并从容器 `/etc/resolv.conf` 读取解析器,HTTPS 时同时监视证书更新;Docker 与 Podman 均无需固定 DNS 地址。修改运行配置后重启后端,即使容器 IP 变化,代理也会自动恢复连接。
267
280
 
268
281
  同前缀但由进程直接读取的环境标记、初始账号密码和传输密钥环仍保留;它们不属于已被父对象替代的配置。
269
282
 
270
- 交付配置会清除所有被 `HAI_*` 父级变量遮蔽的子项,并从 `.env.runtime` 删除由 `middleware.env` 提供的重复项。容器内路径只保存在交付配置中;清理不会改写源环境。修改时以保留下来的父对象或托管连接文件为准。
283
+ 生成端优先写细项,数组保留 JSON;手写 JSON 允许兜底,放在对应模块细项之前。同源细项覆盖 JSON,动态字典、JSON 文本和显式空对象保留原义;整体替换展开时以最上层 `{}` 标记阻止旧 YAML 字段恢复。容器路径只写交付配置,不反写工程源环境。
284
+
285
+ 部署归档只保留五个顶层入口:`apply.sh`、`manifest.json`、`config/`、`images/`、`.hai/`。`config/.env.runtime` 是唯一应用环境文件;`manifest.json.metadata` 集中保存依赖、布局、健康路径、镜像列表、受管绑定等非敏感元数据。`.hai/compose.json` 是完整服务快照;局部更新在服务边界替换并保留未选服务,不串联历史 Compose 文件。`.hai/` 还存原生环境、加载器、Nginx/TLS、策略和校验和,秘密不写入 manifest。辅助脚本合入 `apply.sh`。失败时先恢复原应用配置再恢复服务,凭据、后端账本和数据卷继续保护。新包格式为 2,不实现旧目录兼容或迁移。 运行配置定稿复用本包已导入的 Node 应用镜像,临时容器禁网,不额外下载镜像;服务器无需安装 Node 或 jq。
271
286
 
272
- 框架持久配置使用 `HAI_*` 命名。`POSTGRES_*`、`MYSQL_*`、`RUSTFS_*`、`MINIO_*`、`LDAP_*` 是中间件镜像的原生协议,必须保留原名,但只生成到私有的 `middleware-runtime.env` 并由中间件容器加载;不再写入工程环境文件或应用的 `.env.runtime`。应用增量部署沿用的 `middleware.env` 仅保存框架 `HAI_*` 配置。旧工程已保存的这些原生字段不会被自动删除,新应用环境会忽略它们;新包与旧成功基线可共存,旧服务仍读取其原交付文件。
287
+ 中间件原生变量(`POSTGRES_*`、`MYSQL_*`、`RUSTFS_*`、`MINIO_*`、`LDAP_*`)按镜像契约保留原名,只写私有 `.hai/middleware-runtime.env`,仅由中间件读取。应用连接全部合并到 `config/.env.runtime`。
273
288
 
274
289
  自建或云开通的整体 `HAI_*` 配置会清除被替换的旧叶字段,例如 Redis 覆盖后不再留下 `HAI_CACHE_TYPE=memory`。全局 `~/.hai/credentials.yml` 只补缺项,不能覆盖当前环境/进程的同名键或父配置。部署先启动选中中间件,等待 PostgreSQL/MySQL/Redis/S3 健康,再启动 serv/Web;增量和失败恢复沿用此顺序,未选容器保持不变。
275
290
 
@@ -277,9 +292,9 @@ Podman 部署会主动执行容器已有的 healthcheck 并重新核对状态,
277
292
 
278
293
  分批启动仅在首批清理孤立容器,后续 serv/Web 启动保留已经就绪的中间件。
279
294
 
280
- 生成时为 `.env.dev/.env.test/.env.prod` 分别生成独立的初始管理员密码和传输密钥环;环境文件随项目提交,克隆后可共享,并可通过本机私有文件或进程环境覆盖。启用 transport 的应用缺少/错误配置 `HAI_CRYPTO_TRANSPORT_SERVERKEYRING` 时,部署在上传前指出配置来源;同一环境的所有节点复用同一有效密钥环,可直接加载环境文件,也可通过 Secret Manager 覆盖;不为每个节点临时生成。CLI 完成部署后提示 `HAI_ADMIN_DEFAULT_PASSWORD` 的实际来源,不打印密码,不依赖日志级别;该密码仅在首次创建管理员时生效。 离线入口也会提示服务器私有 `.env.runtime` 中的密码变量位置。
295
+ 创建工程时为 `.env.dev.local`、`.env.test.local`、`.env.prod.local` 分别生成独立初始密码和传输密钥环,文件不提交 Git。克隆工程后通过受保护文件或 Secret Manager 分发对应环境秘密,同环境所有节点使用同一 ring 与共享 registry,不在节点启动时生成。缺少或无效的 `HAI_CRYPTO_TRANSPORT_SERVERKEYRING` 在上传前报错。CLI 和离线说明只提示初始密码变量及来源,不输出值;密码仅用于创建缺失账号,已有账号不被重置。
281
296
 
282
- 每次交付仍生成 `.hai/deploy/<环境>/bundle-.../.env.runtime`,供后端启动预加载(静态 Web 仍用 Compose env_file);它是生成产物,不是配置记忆源。`NODE_ENV` 默认随目标环境选择:`test` → `test`,`dev/development` → `development`,`prod/production` 及其他模式 → `production`。环境文件中显式设置的 `NODE_ENV` 仍优先于该默认值;`--build` 的构建优化模式不决定运行环境。旧交付包不会自动改写,需重新打包。
297
+ 每次交付仍生成 归档内 `config/.env.runtime`,供后端启动预加载(静态 Web 仍用 Compose env_file);它是生成产物,不是配置记忆源。`NODE_ENV` 默认随目标环境选择:`test` → `test`,`dev/development` → `development`,`prod/production` 及其他模式 → `production`。环境文件中显式设置的 `NODE_ENV` 仍优先于该默认值;`--build` 的构建优化模式不决定运行环境。旧交付包不会自动改写,需重新打包。
283
298
 
284
299
  项目根 `hai.json.deploy` 保存部署结构,`.env.<环境>` 保存公共配置,`.env.<环境>.local` 保存 SSH 及中间件秘密值。环境文件默认来自项目/workspace 根,`--env-dir` 可指定目录。完整配置后可使用 `pnpm run deploy --env test --build --bundle-only --yes` 无交互复用;只想保留现有中间件配置时追加 `--skip-middleware`。交互式仅打包会在最后询问是否保存或修改 SSH 信息;修改交付目录的 `deploy-target.json` 不会回写工程。
285
300
 
@@ -1,13 +1,15 @@
1
- /** Node 后端默认启动预加载:每次启动读取发布环境,再加载服务器托管连接配置。 */
1
+ /** 应用仅加载一份运行配置;部署阶段在隔离 Node 容器内定稿连接与托管密码。 */
2
2
  import { Buffer } from 'node:buffer'
3
3
  import { closeSync, constants, fstatSync, openSync, readSync } from 'node:fs'
4
+ import { resolve } from 'node:path'
4
5
  import process from 'node:process'
6
+ import { fileURLToPath } from 'node:url'
5
7
  import { parseEnv } from 'node:util'
6
8
 
7
9
  const maximumBytes = 1024 * 1024
8
10
 
9
- /** 复用 Node dotenv 解析;拒绝特殊文件、链接、超限与不可读文件,不输出秘密。 */
10
- function readEnvironment(name) {
11
+ /** 拒绝特殊文件、链接、超限与不可读文件,不输出秘密。 */
12
+ function readText(name) {
11
13
  let descriptor
12
14
  try {
13
15
  descriptor = openSync(new URL(name, import.meta.url), constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0))
@@ -29,30 +31,162 @@ function readEnvironment(name) {
29
31
  }
30
32
  if (length > maximumBytes)
31
33
  throw new Error('Runtime environment file exceeds 1 MiB')
32
- try {
33
- const text = buffer.subarray(0, length).toString('utf8')
34
- const values = parseEnv(text)
35
- // 交付序列化对同时含单/双引号的字符串使用 JSON 转义;避免 Node 截断转义引号。
36
- for (const match of text.matchAll(/^(?:export\s+)?([A-Za-z_]\w*)\s*=\s*("(?:[^"\\\r\n]|\\.)*")\s*(?:#[^\r\n]*)?$/gm)) {
34
+ return buffer.subarray(0, length).toString('utf8')
35
+ }
36
+ finally {
37
+ closeSync(descriptor)
38
+ }
39
+ }
40
+
41
+ /** 使用与启动入口一致的 dotenv 字面规则,保留混合引号、UTF-8 和多行值。 */
42
+ function readEnvironment(name) {
43
+ if (name === '-')
44
+ return {}
45
+ try {
46
+ const text = readText(name)
47
+ const values = parseEnv(text)
48
+ const names = new Set()
49
+ for (const match of text.matchAll(/^(?:export\s+)?([A-Z_]\w*)\s*=\s*('[^']*'|"(?:[^"\\]|\\.)*"|`[^`]*`|[^\r\n]*)/gim)) {
50
+ if (names.has(match[1]))
51
+ throw new Error('Duplicate environment variable')
52
+ names.add(match[1])
53
+ if (match[2].startsWith('"') && (match[2].includes('\\"') || match[2].includes('\\u0027'))) {
37
54
  try {
38
55
  values[match[1]] = JSON.parse(match[2])
39
56
  }
40
57
  catch { /* 普通 dotenv 双引号仍按 Node 原生规则解析。 */ }
41
58
  }
42
- return values
43
59
  }
44
- catch {
45
- throw new Error('Runtime environment file could not be parsed')
60
+ return values
61
+ }
62
+ catch { throw new Error('Runtime environment file could not be parsed') }
63
+ }
64
+
65
+ /** 清单只保存非敏感元数据,不把环境值放入错误消息。 */
66
+ function readManifest(name) {
67
+ if (name === '-')
68
+ return { metadata: {} }
69
+ try {
70
+ const value = JSON.parse(readText(name))
71
+ if (!value || typeof value !== 'object' || Array.isArray(value) || !value.metadata || typeof value.metadata !== 'object' || Array.isArray(value.metadata)
72
+ || Object.values(value.metadata).some(item => typeof item !== 'string')) {
73
+ throw new Error('Invalid manifest')
46
74
  }
75
+ return value
47
76
  }
48
- finally {
49
- closeSync(descriptor)
77
+ catch { throw new Error('Deployment manifest could not be parsed') }
78
+ }
79
+
80
+ /** 服务器凭据只在部署时注入最终配置;运行启动器不再读取第二份覆盖文件。 */
81
+ function applyManagedPasswords(values, bindings, state) {
82
+ for (const line of bindings.split('\n').filter(Boolean)) {
83
+ const [group, prefix, scope] = line.split(' ')
84
+ if (scope !== 'prefix' || !Object.keys(values).some(key => key === prefix || key.startsWith(`${prefix}_`)))
85
+ continue
86
+ if (!['DB_PASSWORD', 'STORAGE_PASSWORD', 'LDAP_PASSWORD'].includes(group) || !state[group])
87
+ throw new Error('Managed middleware credential state is missing')
88
+ let password = state[group]
89
+ if (state.FORMAT_VERSION === '2') {
90
+ if (!/^(?:[a-f0-9]{2})+$/.test(password))
91
+ throw new Error('Managed middleware credential encoding is invalid')
92
+ password = Buffer.from(password, 'hex').toString('utf8')
93
+ }
94
+ let configuration = {}
95
+ const parent = Object.hasOwn(values, prefix)
96
+ if (parent) {
97
+ try {
98
+ configuration = JSON.parse(values[prefix])
99
+ if (!configuration || typeof configuration !== 'object' || Array.isArray(configuration))
100
+ throw new Error('Invalid object')
101
+ }
102
+ catch { throw new Error(`Invalid managed application configuration: ${prefix}`) }
103
+ }
104
+ const passwordField = prefix === 'HAI_STORAGE' ? 'secretAccessKey' : prefix === 'HAI_IAM_LDAP' ? 'bindPassword' : 'password'
105
+ if (!['HAI_STORAGE', 'HAI_IAM_LDAP', 'HAI_DB', 'HAI_VECDB'].includes(prefix))
106
+ throw new Error('Unsupported managed middleware binding')
107
+ const url = values[`${prefix}_URL`] ?? configuration.url
108
+ if (['HAI_DB', 'HAI_VECDB'].includes(prefix) && url) {
109
+ try {
110
+ const connection = new URL(url)
111
+ connection.password = encodeURIComponent(password)
112
+ values[`${prefix}_URL`] = connection.toString()
113
+ }
114
+ catch { throw new Error(`Invalid managed application connection: ${prefix}`) }
115
+ }
116
+ else {
117
+ values[`${prefix}_${passwordField.toUpperCase()}`] = password
118
+ }
50
119
  }
51
120
  }
52
121
 
53
- // 托管连接优先级沿用原 env_file 顺序;删除应用字段不会从旧容器环境复活。
54
- const values = { ...readEnvironment('./.env.runtime'), ...readEnvironment('./middleware.env') }
55
- // 启动参数与私有代理身份属于部署装配,不由可编辑的业务环境覆盖。
56
- delete values.NODE_OPTIONS
57
- delete values.HAI_DEPLOY_PRIVATE_PROXY
58
- Object.assign(process.env, values)
122
+ /** 保留父 JSON 与显式细项;粗细覆盖由应用通用配置解析器完成。 */
123
+ function serialize(values) {
124
+ return `${Object.entries(values).sort(([left], [right]) => left.localeCompare(right)).filter(([name]) => name !== 'NODE_OPTIONS' && name !== 'HAI_DEPLOY_PRIVATE_PROXY').map(([name, value]) => {
125
+ const literal = !value.includes('\'') ? `'${value}'` : JSON.stringify(value).replaceAll('\'', '\\u0027')
126
+ return `${name}=${literal}`
127
+ }).join('\n')}\n`
128
+ }
129
+
130
+ /** 局部后端发布沿用当前连接,完整发布采用新配置;不改动托管凭据状态。 */
131
+ function prepare(directory, previousManifest, previousEnvironment, credentialState, mode) {
132
+ if (!['full', 'partial'].includes(mode))
133
+ throw new Error('Invalid runtime configuration preparation mode')
134
+ const base = new URL(directory, import.meta.url)
135
+ const incoming = readManifest(new URL('manifest.json', base))
136
+ const previous = mode === 'full' ? { metadata: {} } : readManifest(previousManifest)
137
+ const before = mode === 'full' ? {} : readEnvironment(previousEnvironment)
138
+ const next = readEnvironment(new URL('config/.env.runtime', base))
139
+ const entries = key => [...new Set(`${previous.metadata[key] ?? ''}\n${incoming.metadata[key] ?? ''}`.split('\n').filter(Boolean))]
140
+ const keys = entries('managed-app-keys')
141
+ if (keys.some(key => !/^HAI_[A-Z0-9_]+$/.test(key)))
142
+ throw new Error('Invalid managed application environment key')
143
+ if (mode === 'partial') {
144
+ for (const key of keys) {
145
+ const retained = Object.keys(before).filter(name => name === key || name.startsWith(`${key}_`))
146
+ if (retained.length) {
147
+ for (const name of Object.keys(next)) {
148
+ if (name === key || name.startsWith(`${key}_`))
149
+ delete next[name]
150
+ }
151
+ for (const name of retained)
152
+ next[name] = before[name]
153
+ }
154
+ }
155
+ }
156
+ const bindings = entries('managed-bindings').join('\n')
157
+ const state = readEnvironment(credentialState)
158
+ const project = incoming.metadata['managed-project'] ?? previous.metadata['managed-project']
159
+ if (state.PROJECT && project && state.PROJECT !== project.trim())
160
+ throw new Error('Managed middleware credential project mismatch')
161
+ applyManagedPasswords(next, bindings, state)
162
+ incoming.metadata['managed-app-keys'] = `${keys.join('\n')}${keys.length ? '\n' : ''}`
163
+ incoming.metadata['managed-bindings'] = `${bindings}${bindings ? '\n' : ''}`
164
+ if (project)
165
+ incoming.metadata['managed-project'] = project
166
+ const environment = serialize(next)
167
+ if (Buffer.byteLength(environment) > maximumBytes)
168
+ throw new Error('Prepared runtime environment exceeds 1 MiB')
169
+ return { environment, manifest: incoming }
170
+ }
171
+
172
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
173
+ if (process.argv[2] !== 'prepare' || process.argv.length !== 8)
174
+ throw new Error('Runtime preparation requires a release directory, previous manifest, current environment, credential state and mode')
175
+ process.stdout.write(JSON.stringify(prepare(...process.argv.slice(3))))
176
+ }
177
+ else {
178
+ const values = readEnvironment('./config/.env.runtime')
179
+ // 启动参数与私有代理身份属于部署装配,不由业务运行文件覆盖。
180
+ delete values.NODE_OPTIONS
181
+ delete values.HAI_DEPLOY_PRIVATE_PROXY
182
+ // HAI 配置的显式进程来源最高;镜像内 NODE_ENV/HOST/PORT 默认值由运行文件定稿。
183
+ const inherited = Object.fromEntries(Object.entries(process.env).filter(([name, value]) => name.startsWith('HAI_') && value !== undefined))
184
+ for (const name of Object.keys(inherited).sort((left, right) => left.length - right.length)) {
185
+ for (const previous of Object.keys(values)) {
186
+ if (previous.startsWith(`${name}_`))
187
+ delete values[previous]
188
+ }
189
+ values[name] = inherited[name]
190
+ }
191
+ Object.assign(process.env, values)
192
+ }