@h-ai/cli 0.1.0-alpha.50 → 0.1.0-alpha.53

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.
Files changed (34) hide show
  1. package/dist/index.js +155 -2
  2. package/dist/index.js.map +1 -1
  3. package/package.json +2 -2
  4. package/templates/apps/api/.dockerignore.hbs +14 -0
  5. package/templates/apps/api/Dockerfile.hbs +37 -0
  6. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +6 -0
  7. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +2 -2
  8. package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +4 -4
  9. package/templates/apps/api/apps/{{projectName}}-service/data/.gitkeep.hbs +1 -0
  10. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +2 -0
  11. package/templates/apps/api/package.json.hbs +4 -0
  12. package/templates/apps/fullstack/.dockerignore.hbs +16 -0
  13. package/templates/apps/fullstack/Dockerfile.hbs +58 -0
  14. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +2 -1
  15. package/templates/apps/fullstack/apps/{{projectName}}-web/public/runtime-config.js.hbs +1 -0
  16. package/templates/apps/fullstack/apps/{{projectName}}-web/tests/api.test.ts.hbs +27 -0
  17. package/templates/apps/fullstack/docker/19-runtime-config.envsh.hbs +5 -0
  18. package/templates/apps/fullstack/docker/nginx.conf.hbs +24 -0
  19. package/templates/apps/fullstack/docker/runtime-config.js.template.hbs +3 -0
  20. package/templates/apps/fullstack/package.json.hbs +6 -0
  21. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +8 -0
  22. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +2 -2
  23. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +3 -3
  24. package/templates/apps/fullstack/packages/{{projectName}}-serv/data/.gitkeep.hbs +1 -0
  25. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  26. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +10 -1
  27. package/templates/base/.dockerignore.hbs +17 -0
  28. package/templates/base/.gitignore.hbs +2 -1
  29. package/templates/base/Dockerfile.hbs +62 -0
  30. package/templates/base/docker/nginx.conf.hbs +18 -0
  31. package/templates/base/package.json.hbs +16 -6
  32. package/templates/base/svelte.config.js.hbs +0 -8
  33. package/templates/skills/hai-ai/SKILL.md +4 -0
  34. package/templates/skills/hai-app-create/SKILL.md +15 -0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/cli",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.50",
4
+ "version": "0.1.0-alpha.53",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -34,7 +34,7 @@
34
34
  "ora": "^9.1.0",
35
35
  "prompts": "^2.4.2",
36
36
  "yaml": "^2.7.0",
37
- "@h-ai/core": "0.1.0-alpha.50"
37
+ "@h-ai/core": "0.1.0-alpha.53"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/fs-extra": "^11.0.4",
@@ -0,0 +1,14 @@
1
+ .git
2
+ .github
3
+ .agents
4
+ .claude
5
+ .codex
6
+ .opencode
7
+ .turbo
8
+ build
9
+ dist
10
+ node_modules
11
+ playwright-report
12
+ test-results
13
+ **/data
14
+ *.log
@@ -0,0 +1,37 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM node:22-bookworm-slim AS build
3
+
4
+ ENV PNPM_HOME=/pnpm
5
+ ENV PATH=$PNPM_HOME:$PATH
6
+ ENV CI=true
7
+ WORKDIR /workspace
8
+
9
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
10
+ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
11
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --frozen-lockfile
12
+ COPY . .
13
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
14
+ RUN pnpm build
15
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm --filter {{api.servicePackageName}} --prod deploy /prod/app
16
+
17
+ FROM node:22-bookworm-slim AS runtime-base
18
+
19
+ ENV NODE_ENV=production
20
+ ENV HOST=0.0.0.0
21
+ ENV PORT=3000
22
+ WORKDIR /app
23
+
24
+ COPY --from=build --chown=node:node /prod/app ./
25
+ RUN mkdir -p /app/data && chown node:node /app/data
26
+
27
+ USER node
28
+ EXPOSE 3000
29
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
30
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
31
+ CMD ["node", "dist/index.js"]
32
+
33
+ FROM runtime-base AS runtime-with-data
34
+
35
+ COPY --from=embedded-data --chown=node:node / /app/data/
36
+
37
+ FROM runtime-base AS runtime
@@ -9,6 +9,12 @@ pnpm --filter {{api.servicePackageName}} dev
9
9
  pnpm --filter {{api.servicePackageName}} test
10
10
  ```
11
11
 
12
+ ## 容器运行
13
+
14
+ - 根目录运行 `pnpm docker:build` / `pnpm podman:build`;镜像监听 `0.0.0.0:3000`。
15
+ - 配置默认打包自本目录 `config/`,可在运行时只读挂载到 `/app/config`,敏感值通过环境变量注入。
16
+ - `data/` 可作为 SQLite/本地存储目录;`pnpm docker:build:data` / `pnpm podman:build:data` 会将其固化为镜像初始数据,生产写入应挂载 `/app/data` volume。
17
+
12
18
  ## 关键目录
13
19
 
14
20
  - `config/`:service 配置
@@ -4,8 +4,8 @@
4
4
 
5
5
  name: {{projectName}} API Service
6
6
  version: 0.1.0
7
- env: development
8
- debug: false
7
+ env: ${HAI_ENV:development}
8
+ debug: ${HAI_DEBUG:false}
9
9
 
10
10
  defaultLocale: {{defaultLocale}}
11
11
  supportedLocales:
@@ -18,10 +18,10 @@ http:
18
18
  rpc: false
19
19
 
20
20
  cors:
21
- origin: '*'
22
- nativeOrigins: http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost
23
- {{#if features.iam}} credentials: true
24
- {{else}} credentials: false
21
+ origin: ${HAI_CORS_ORIGIN:*}
22
+ nativeOrigins: ${HAI_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
23
+ {{#if features.iam}} credentials: ${HAI_CORS_CREDENTIALS:true}
24
+ {{else}} credentials: ${HAI_CORS_CREDENTIALS:false}
25
25
  {{/if}} allowedHeaders:
26
26
  - Authorization
27
27
  - Content-Type
@@ -14,6 +14,8 @@
14
14
  "types": "./dist/index.d.ts",
15
15
  "files": [
16
16
  "dist",
17
+ "config",
18
+ "data",
17
19
  "README.md"
18
20
  ],
19
21
  "scripts": {
@@ -9,6 +9,10 @@
9
9
  "dev": "pnpm --filter {{api.servicePackageName}} dev",
10
10
  "compile": "pnpm typecheck && pnpm build",
11
11
  "build": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present build",
12
+ "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
13
+ "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
14
+ "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest .",
15
+ "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./apps/{{projectName}}-service/data -t {{projectName}}:latest .",
12
16
  "typecheck": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present typecheck",
13
17
  "lint": "pnpm -r --if-present lint",
14
18
  "test": "pnpm --filter {{api.contractPackageName}} build && pnpm -r --if-present test",
@@ -0,0 +1,16 @@
1
+ .git
2
+ Dockerfile
3
+ .github
4
+ .agents
5
+ .claude
6
+ .codex
7
+ .opencode
8
+ .turbo
9
+ **/build
10
+ **/dist
11
+ **/node_modules
12
+ node_modules
13
+ playwright-report
14
+ test-results
15
+ **/data
16
+ *.log
@@ -0,0 +1,58 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ FROM node:22-bookworm-slim AS build
3
+
4
+ ENV PNPM_HOME=/pnpm
5
+ ENV PATH=$PNPM_HOME:$PATH
6
+ ENV CI=true
7
+ WORKDIR /workspace
8
+
9
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
10
+ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
11
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --frozen-lockfile
12
+ COPY . .
13
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
14
+ RUN pnpm build
15
+
16
+ FROM build AS service-build
17
+
18
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm --filter {{fullstack.servPackageName}} --prod deploy /prod/service
19
+
20
+ FROM node:22-bookworm-slim AS service
21
+
22
+ ENV NODE_ENV=production
23
+ ENV HOST=0.0.0.0
24
+ ENV PORT=3000
25
+ WORKDIR /app
26
+
27
+ COPY --from=service-build --chown=node:node /prod/service ./
28
+ RUN mkdir -p /app/data && chown node:node /app/data
29
+
30
+ USER node
31
+ EXPOSE 3000
32
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
33
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
34
+ CMD ["node", "dist/index.js"]
35
+
36
+ FROM service AS runtime-with-data
37
+
38
+ COPY --from=embedded-data --chown=node:node / /app/data/
39
+
40
+ FROM service AS runtime
41
+
42
+ {{#if fullstack.frontends.web}}
43
+ FROM nginxinc/nginx-unprivileged:1.29-alpine AS web
44
+
45
+ ENV PUBLIC_API_BASE=
46
+ ENV NGINX_ENVSUBST_FILTER=PUBLIC_API_BASE_BASE64
47
+ ENV NGINX_ENVSUBST_OUTPUT_DIR=/tmp/hai-runtime-config
48
+
49
+ RUN mkdir -p /tmp/hai-runtime-config
50
+ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
51
+ COPY --chmod=755 docker/19-runtime-config.envsh /docker-entrypoint.d/19-runtime-config.envsh
52
+ COPY docker/runtime-config.js.template /etc/nginx/templates/runtime-config.js.template
53
+ COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
54
+
55
+ EXPOSE 8080
56
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
57
+ CMD wget -Y off -q -O /dev/null http://127.0.0.1:8080/health || exit 1
58
+ {{/if}}
@@ -6,10 +6,11 @@
6
6
  <meta charset="UTF-8" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
8
  <title>{{projectName}} Web</title>
9
+ <script src="/runtime-config.js"></script>
9
10
  </head>
10
11
  <body>
11
12
  <div id="app"></div>
12
13
  <script type="module" src="/src/main.ts"></script>
13
14
  </body>
14
15
  </html>
15
- {{/if}}
16
+ {{/if}}
@@ -0,0 +1 @@
1
+ globalThis.__HAI_PUBLIC_CONFIG__ = {}
@@ -24,5 +24,32 @@ describe('fetchAppInfo', () => {
24
24
 
25
25
  expect(info.name).toBe('{{fullstack.servPackageName}}')
26
26
  })
27
+
28
+ it('uses API origin injected at container startup', async () => {
29
+ Object.assign(globalThis, {
30
+ __HAI_PUBLIC_CONFIG__: { apiBase: 'https://api.example.com' },
31
+ })
32
+ let requestedUrl = ''
33
+ const fetchFn: typeof fetch = async (input) => {
34
+ requestedUrl = input instanceof Request ? input.url : String(input)
35
+ return new Response(JSON.stringify({
36
+ success: true,
37
+ data: {
38
+ name: '{{fullstack.servPackageName}}',
39
+ version: '0.1.0',
40
+ startedAt: '2026-01-01T00:00:00.000Z',
41
+ frontends: ['web'],
42
+ },
43
+ }), { headers: { 'content-type': 'application/json' } })
44
+ }
45
+
46
+ try {
47
+ await fetchAppInfo({ fetchFn })
48
+ expect(requestedUrl).toContain('https://api.example.com/api/v1')
49
+ }
50
+ finally {
51
+ Reflect.deleteProperty(globalThis, '__HAI_PUBLIC_CONFIG__')
52
+ }
53
+ })
27
54
  })
28
55
  {{/if}}
@@ -0,0 +1,5 @@
1
+ #!/bin/sh
2
+
3
+ # 使用 Base64 跨越 shell、envsubst 与 JavaScript 字符串边界,避免 URL 字符改变脚本语义。
4
+ PUBLIC_API_BASE_BASE64="$(printf '%s' "${PUBLIC_API_BASE:-}" | base64 | tr -d '\n')"
5
+ export PUBLIC_API_BASE_BASE64
@@ -0,0 +1,24 @@
1
+ {{! @skipIfEmpty }}
2
+ {{#if fullstack.frontends.web}}
3
+ server {
4
+ listen 8080;
5
+ server_name _;
6
+ root /usr/share/nginx/html;
7
+
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ location = /runtime-config.js {
13
+ alias /tmp/hai-runtime-config/runtime-config.js;
14
+ add_header Cache-Control "no-store";
15
+ default_type application/javascript;
16
+ }
17
+
18
+ location = /health {
19
+ access_log off;
20
+ add_header Content-Type text/plain;
21
+ return 200 'ok';
22
+ }
23
+ }
24
+ {{/if}}
@@ -0,0 +1,3 @@
1
+ globalThis.__HAI_PUBLIC_CONFIG__ = {
2
+ apiBase: new TextDecoder().decode(Uint8Array.from(atob('${PUBLIC_API_BASE_BASE64}'), value => value.charCodeAt(0))),
3
+ }
@@ -11,6 +11,12 @@
11
11
  "dev": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev",
12
12
  "compile": "pnpm typecheck && pnpm build",
13
13
  "build": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present build",
14
+ "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
15
+ "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./packages/{{projectName}}-serv/data -t {{projectName}}:latest ."{{#if fullstack.frontends.web}},
16
+ "docker:build:web": "docker build --target web -t {{projectName}}-web:latest ."{{/if}},
17
+ "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest .",
18
+ "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./packages/{{projectName}}-serv/data -t {{projectName}}:latest ."{{#if fullstack.frontends.web}},
19
+ "podman:build:web": "podman build --format docker --jobs=1 --target web -t {{projectName}}-web:latest ."{{/if}},
14
20
  "package": "pnpm build{{#if fullstack.frontends.web}} && pnpm --filter {{projectName}}-web package{{/if}}{{#if fullstack.frontends.app}} && pnpm --filter {{projectName}}-app package{{/if}}{{#if fullstack.frontends.desktop}} && pnpm --filter {{projectName}}-desktop package{{/if}}",
15
21
  "deploy": "pnpm package",
16
22
  "typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
@@ -15,3 +15,11 @@
15
15
 
16
16
  - `pnpm --filter {{fullstack.servPackageName}} dev`
17
17
  - `pnpm --filter {{fullstack.servPackageName}} test`
18
+
19
+ ## 容器运行
20
+
21
+ - 根目录 `pnpm docker:build` / `pnpm podman:build` 直接生成本服务镜像;Web 使用独立的 `docker:build:web` / `podman:build:web` 镜像。
22
+ - 服务镜像直接由 `@h-ai/serv` 对外监听,配置挂载点为 `/app/config`,数据目录为 `/app/data`,不包含额外转发进程。
23
+ - Web 镜像不烘焙 API 地址;启动时使用 `-e PUBLIC_API_BASE=https://api.example.com` 注入,同一镜像可部署到不同环境。未设置时 Web 默认请求自身 origin。
24
+ - YAML 配置使用 `${ENV_NAME:default}` 接收运行时环境变量;不要把密钥写入镜像。
25
+ - 将 SQLite/本地存储放入本目录 `data/` 后运行 `pnpm docker:build:data` / `pnpm podman:build:data`,可生成带初始数据的镜像,持续写入仍应使用 volume。
@@ -8,7 +8,7 @@
8
8
 
9
9
  name: {{fullstack.servPackageName}}
10
10
  version: 0.1.0
11
- env: development
12
- debug: false
11
+ env: ${HAI_ENV:development}
12
+ debug: ${HAI_DEBUG:false}
13
13
 
14
14
  defaultLocale: {{fullstack.i18n.baseLocale}}
@@ -21,9 +21,9 @@ http:
21
21
  rpc: false
22
22
 
23
23
  cors:
24
- origin: '*'
25
- nativeOrigins: http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost
26
- credentials: false
24
+ origin: ${HAI_CORS_ORIGIN:*}
25
+ nativeOrigins: ${HAI_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
26
+ credentials: ${HAI_CORS_CREDENTIALS:false}
27
27
  allowedHeaders:
28
28
  - Authorization
29
29
  - Content-Type
@@ -11,6 +11,12 @@
11
11
  },
12
12
  "main": "./dist/index.js",
13
13
  "types": "./dist/index.d.ts",
14
+ "files": [
15
+ "config",
16
+ "data",
17
+ "dist",
18
+ "README.md"
19
+ ],
14
20
  "scripts": {
15
21
  "build": "tsup",
16
22
  "typecheck": "tsc --noEmit",
@@ -38,7 +38,16 @@ export async function fetchAppInfo(options: FetchAppInfoOptions = {}): Promise<A
38
38
  }
39
39
 
40
40
  function resolveDefaultApiOrigin(): string {
41
- return import.meta.env.PUBLIC_API_BASE ?? DEFAULT_API_ORIGIN
41
+ return resolveRuntimeApiOrigin()
42
+ || import.meta.env.PUBLIC_API_BASE
43
+ || (typeof globalThis.location === 'undefined' ? DEFAULT_API_ORIGIN : globalThis.location.origin)
44
+ }
45
+
46
+ function resolveRuntimeApiOrigin(): string | undefined {
47
+ const runtimeConfig = (globalThis as typeof globalThis & {
48
+ __HAI_PUBLIC_CONFIG__?: { apiBase?: string }
49
+ }).__HAI_PUBLIC_CONFIG__
50
+ return runtimeConfig?.apiBase
42
51
  }
43
52
 
44
53
  function normalizeApiBaseUrl(value: string): string {
@@ -0,0 +1,17 @@
1
+ .git
2
+ .github
3
+ .agents
4
+ .claude
5
+ .codex
6
+ .opencode
7
+ .svelte-kit
8
+ .turbo
9
+ android
10
+ build
11
+ dist
12
+ ios
13
+ node_modules
14
+ playwright-report
15
+ test-results
16
+ **/data
17
+ *.log
@@ -13,4 +13,5 @@ ios
13
13
  .DS_Store
14
14
  test-results/
15
15
  playwright-report/
16
- data/
16
+ data/*
17
+ !**/data/.gitkeep
@@ -0,0 +1,62 @@
1
+ # syntax=docker/dockerfile:1.7
2
+ {{#if isSvelteOnlyApp}}
3
+ FROM node:22-bookworm-slim AS build
4
+
5
+ ENV PNPM_HOME=/pnpm
6
+ ENV PATH=$PNPM_HOME:$PATH
7
+ ENV CI=true
8
+ WORKDIR /workspace
9
+
10
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
11
+ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
12
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --frozen-lockfile
13
+ COPY . .
14
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
15
+ RUN pnpm build
16
+
17
+ FROM nginxinc/nginx-unprivileged:1.29-alpine AS runtime
18
+
19
+ COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
20
+ COPY --from=build /workspace/dist /usr/share/nginx/html
21
+
22
+ EXPOSE 8080
23
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
24
+ CMD wget -q -O /dev/null http://127.0.0.1:8080/health || exit 1
25
+ {{else}}
26
+ FROM node:22-bookworm-slim AS build
27
+
28
+ ENV PNPM_HOME=/pnpm
29
+ ENV PATH=$PNPM_HOME:$PATH
30
+ ENV CI=true
31
+ WORKDIR /workspace
32
+
33
+ RUN corepack enable && corepack prepare pnpm@9.15.0 --activate
34
+ COPY pnpm-lock.yaml pnpm-workspace.yaml ./
35
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --frozen-lockfile
36
+ COPY . .
37
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
38
+ RUN pnpm build
39
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm --filter {{projectName}} --prod deploy /prod/app
40
+
41
+ FROM node:22-bookworm-slim AS runtime-base
42
+
43
+ ENV NODE_ENV=production
44
+ ENV HOST=0.0.0.0
45
+ ENV PORT=3000
46
+ WORKDIR /app
47
+
48
+ COPY --from=build --chown=node:node /prod/app ./
49
+ RUN mkdir -p /app/data && chown node:node /app/data
50
+
51
+ USER node
52
+ EXPOSE 3000
53
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
54
+ CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
55
+ CMD ["node", "build"]
56
+
57
+ FROM runtime-base AS runtime-with-data
58
+
59
+ COPY --from=embedded-data --chown=node:node / /app/data/
60
+
61
+ FROM runtime-base AS runtime
62
+ {{/if}}
@@ -0,0 +1,18 @@
1
+ {{! @skipIfEmpty }}
2
+ {{#if isSvelteOnlyApp}}
3
+ server {
4
+ listen 8080;
5
+ server_name _;
6
+ root /usr/share/nginx/html;
7
+
8
+ location / {
9
+ try_files $uri $uri/ /index.html;
10
+ }
11
+
12
+ location = /health {
13
+ access_log off;
14
+ add_header Content-Type text/plain;
15
+ return 200 'ok';
16
+ }
17
+ }
18
+ {{/if}}
@@ -1,11 +1,19 @@
1
1
  {
2
2
  "name": "{{projectName}}",
3
- "version": "0.1.0-alpha.50",
3
+ "version": "0.1.0-alpha.53",
4
4
  "private": true,
5
5
  "description": "{{#if_eq appType 'admin'}}Enterprise management system built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'website'}}Enterprise website built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'h5'}}Mobile H5 app built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'api'}}REST API service built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'mobile-app'}}Mobile app built with hai Agent Framework{{/if_eq}}",
6
6
  "keywords": ["hai", {{#unless isSvelteOnlyApp}}"sveltekit", {{/unless}}"svelte5", "{{appType}}"],
7
7
  "license": "Apache-2.0",
8
8
  "type": "module",
9
+ {{#unless isSvelteOnlyApp}}
10
+ "files": [
11
+ "build",
12
+ "config",
13
+ "data",
14
+ "package.json"
15
+ ],
16
+ {{/unless}}
9
17
  "scripts": {
10
18
  {{#if hasI18n}}
11
19
  "paraglide:compile": "paraglide-js compile --project ./project.inlang --outdir ./src/lib/paraglide",
@@ -19,7 +27,13 @@
19
27
  "test": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vitest run",
20
28
  "test:watch": "vitest",
21
29
  "test:e2e": "playwright test",
22
- "clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}"{{#if isCapacitorApp}},
30
+ "clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}",
31
+ "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
32
+ {{#unless isSvelteOnlyApp}}
33
+ "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest .",
34
+ {{/unless}}
35
+ "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest ."{{#unless isSvelteOnlyApp}},
36
+ "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest ."{{/unless}}{{#if isCapacitorApp}},
23
37
  "cap:add:android": "cap add android",
24
38
  "cap:add:ios": "cap add ios",
25
39
  "cap:sync": "pnpm build && cap sync",
@@ -78,11 +92,7 @@
78
92
  "@capacitor/push-notifications": "^8.0.1",
79
93
  "@capacitor/status-bar": "^8.0.1",
80
94
  {{else}}
81
- {{#if_eq appType 'api'}}
82
95
  "@sveltejs/adapter-node": "^5.5.4",
83
- {{else}}
84
- "@sveltejs/adapter-auto": "^7.0.1",
85
- {{/if_eq}}
86
96
  {{/if}}
87
97
  {{#unless isSvelteOnlyApp}}
88
98
  "@sveltejs/kit": "^2.61.1",
@@ -8,15 +8,7 @@ import process from 'node:process'
8
8
  import { autoImportHaiUi } from '@h-ai/ui/auto-import'
9
9
  {{/if}}
10
10
  {{#unless isSvelteOnlyApp}}
11
- {{#if isCapacitorApp}}
12
- import adapter from '@sveltejs/adapter-static'
13
- {{else}}
14
- {{#if_eq appType 'api'}}
15
11
  import adapter from '@sveltejs/adapter-node'
16
- {{else}}
17
- import adapter from '@sveltejs/adapter-auto'
18
- {{/if_eq}}
19
- {{/if}}
20
12
  {{/unless}}
21
13
  import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'
22
14
 
@@ -337,6 +337,10 @@ if (!playable.success)
337
337
  - 平台不支持的输入方式(如 OpenAI/MiMo 的持续音频输入)会抛 `AUDIO_UNSUPPORTED_INPUT`,不伪装成实时。
338
338
  - 浏览器/移动端经 `@h-ai/serv` 统一语音 WebSocket 入口访问;客户端用已登录 HTTP 请求获取短期一次性 ticket,`@h-ai/ai/client` 通过 `audio.getTicket` 建连,不把 IAM access token 放入 URL。浏览器客户端区分正常结束、取消(`AUDIO_CANCELLED`)与 `end` 前异常断连(`AUDIO_CONNECTION_FAILED`);服务端 error 帧保留领域错误码,`synthesize` 不返回未完成的部分音频。
339
339
 
340
+ ### 本地 CPU 模型服务
341
+
342
+ hai-framework 源码仓库提供 `pnpm model <name> --device cpu` 与 `pnpm model:run <name>`,默认优先使用 Podman,可构建/启动 `faster-whisper-large-v3`、`indextts-2.5`、`qwen3-4b`。三套权重均优先从清单锁定的 ModelScope revision 下载并复用容器 volume,Hugging Face 只作回退。Whisper/IndexTTS 仍配置为 `audio.models[].baseUrl`,Qwen3-4B 仍配置为 OpenAI-compatible `llm.models[].baseUrl`;部署位置不进入 Provider 名称。离线部署使用准备权重后的 bundled 镜像。
343
+
340
344
  ## 文生图(Image)
341
345
 
342
346
  `ai.image` 只暴露稳定的提示词、模型别名、像素尺寸、可选参考图和图片字节;OpenAI、Google、Qwen、Seedream 与 Pollinations 的请求结构、鉴权、响应解析和临时 URL 下载均封装在内部 Provider。
@@ -354,6 +354,20 @@ core.init() → reldb.init() → cache.init() → iam.init() → createBusinessT
354
354
 
355
355
  ---
356
356
 
357
+ ## 容器交付
358
+
359
+ - 保持脚手架根目录 `Dockerfile`、`.dockerignore` 与 `package.json` 中 `docker:build*` / `podman:build*` 命令同步。
360
+ - Podman 多阶段构建保持 `--format docker --jobs=1`;前者保留 `HEALTHCHECK`,后者避免部分 Podman 5.8/Buildah 组合在并行 stage 构建时触发内部 semaphore panic。
361
+ - 先复制 lockfile 与 workspace 配置执行 `pnpm fetch`,再复制源码并离线安装、构建;避免源码变化重复下载依赖,同时兼容新增 workspace。
362
+ - SvelteKit 服务使用 Node adapter;纯 Vite 前端使用带 SPA fallback 的静态镜像;fullstack 的 Service 与 Web 分别交付,Service 直接监听且不增加转发进程。
363
+ - Web 的 `PUBLIC_*` 部署配置在容器启动时生成静态运行时配置,不作为 build arg 烘焙,确保同一镜像可跨环境部署。
364
+ - 配置文件随镜像提供,并允许只读挂载覆盖;密钥只通过环境变量或 `--env-file` 注入。
365
+ - 普通构建不携带 `data/`;只有用户明确运行 `docker:build:data` / `podman:build:data` 时才通过 named build context 嵌入 SQLite/本地存储初始快照。
366
+ - 数据库和本地存储的持续写入必须落到 volume,不能依赖镜像可写层。
367
+ - 完成前实际启动镜像,验证健康端点、非 root 用户、环境变量覆盖、配置挂载和预置数据读取。
368
+
369
+ ---
370
+
357
371
  ## 检查清单
358
372
 
359
373
  - [ ] **TDD 流程已执行**:先写测试 → 确认失败 → 再实现 → 确认通过
@@ -365,6 +379,7 @@ core.init() → reldb.init() → cache.init() → iam.init() → createBusinessT
365
379
  - [ ] 日志级别正确(读 debug / 写 debug+info / 失败 warn|error)
366
380
  - [ ] 禁止 `console.log`、`any`、硬编码密钥
367
381
  - [ ] 用户可见文本走 i18n
382
+ - [ ] Docker / Podman 镜像可启动,配置/环境变量/数据目录行为已验证
368
383
  - [ ] 新增表有索引定义
369
384
  - [ ] 组件优先使用 `@h-ai/ui`
370
385
  - [ ] `pnpm typecheck && pnpm lint && pnpm test` 通过