@h-ai/cli 0.1.0-alpha.65 → 0.1.0-alpha.66

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 (69) hide show
  1. package/README.en.md +13 -7
  2. package/README.ja.md +13 -7
  3. package/README.md +14 -8
  4. package/dist/{chunk-IPG3QNYK.js → chunk-FWN5B54L.js} +36 -4
  5. package/dist/chunk-FWN5B54L.js.map +1 -0
  6. package/dist/{chunk-47V5OT22.js → chunk-QYX6V7H2.js} +17 -43
  7. package/dist/chunk-QYX6V7H2.js.map +1 -0
  8. package/dist/cli-config-templates-3XSFIXBP.js +5 -0
  9. package/dist/{cli-config-templates-SJ2MBIR7.js.map → cli-config-templates-3XSFIXBP.js.map} +1 -1
  10. package/dist/{cli-skill-templates-A5VIR5J6.js → cli-skill-templates-TSYEQTCY.js} +3 -3
  11. package/dist/{cli-skill-templates-A5VIR5J6.js.map → cli-skill-templates-TSYEQTCY.js.map} +1 -1
  12. package/dist/index.js +273 -208
  13. package/dist/index.js.map +1 -1
  14. package/dist/{src-6PPBKQ6Q.js → src-RDQ2EIBT.js} +506 -488
  15. package/dist/src-RDQ2EIBT.js.map +1 -0
  16. package/package.json +8 -8
  17. package/templates/apps/api/.dockerignore.hbs +4 -5
  18. package/templates/apps/api/.gitignore.hbs +2 -1
  19. package/templates/apps/api/Dockerfile.hbs +2 -15
  20. package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +2 -2
  21. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +0 -1
  22. package/templates/apps/api/hai.build.json.hbs +1 -0
  23. package/templates/apps/api/package.json.hbs +5 -8
  24. package/templates/apps/fullstack/.dockerignore.hbs +4 -5
  25. package/templates/apps/fullstack/.gitignore.hbs +2 -1
  26. package/templates/apps/fullstack/Dockerfile.hbs +3 -15
  27. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +0 -3
  28. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +0 -2
  29. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
  30. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -6
  31. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +2 -3
  32. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/src/lib.rs.hbs +73 -2
  33. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +2 -2
  34. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +2 -2
  35. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -6
  36. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +0 -2
  37. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
  38. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +1 -0
  39. package/templates/apps/fullstack/hai.build.json.hbs +7 -0
  40. package/templates/apps/fullstack/package.json.hbs +5 -12
  41. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +2 -2
  42. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +0 -1
  43. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +1 -1
  44. package/templates/apps/mobile-app/index.html.hbs +1 -0
  45. package/templates/apps/mobile-app/public/runtime-config.js +1 -0
  46. package/templates/apps/mobile-app/src/lib/api.ts.hbs +2 -1
  47. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +3 -2
  48. package/templates/base/.dockerignore.hbs +4 -5
  49. package/templates/base/.gitignore.hbs +2 -1
  50. package/templates/base/Dockerfile.hbs +5 -22
  51. package/templates/base/docker/nginx.conf.hbs +6 -0
  52. package/templates/base/hai.build.json.hbs +1 -0
  53. package/templates/base/package.json.hbs +13 -17
  54. package/templates/base/svelte.config.js.hbs +2 -0
  55. package/templates/base/vite.config.ts.hbs +6 -2
  56. package/templates/shared/.gitattributes +5 -0
  57. package/templates/shared/container-runtime-config.sh +30 -0
  58. package/templates/shared/run-e2e.mjs +65 -0
  59. package/templates/shared/vite-runtime-config.ts +54 -0
  60. package/templates/skills/hai-app-create/development.md +3 -1
  61. package/templates/skills/hai-app-create/reference.md +2 -2
  62. package/templates/skills/hai-build/SKILL.md +16 -1
  63. package/templates/skills/hai-deploy/SKILL.md +52 -260
  64. package/templates/skills/hai-deploy/middleware.md +8 -14
  65. package/templates/skills/hai-ui/reference.md +2 -0
  66. package/dist/chunk-47V5OT22.js.map +0 -1
  67. package/dist/chunk-IPG3QNYK.js.map +0 -1
  68. package/dist/cli-config-templates-SJ2MBIR7.js +0 -5
  69. package/dist/src-6PPBKQ6Q.js.map +0 -1
@@ -25,8 +25,9 @@ playwright-report/
25
25
  # 本地数据目录
26
26
  **/data/
27
27
 
28
- # hai deploy 产物(compose / .env.runtime / 镜像 tar / sidecar 凭据)
28
+ # 构建与部署产物(镜像 / Compose tar / 私有运行配置)
29
29
  .hai/
30
+ *.tar
30
31
 
31
32
  # 本机私有配置;公共 .env 与 dev/test/prod 环境文件纳入版本控制。
32
33
  .env.local
@@ -3,10 +3,6 @@
3
3
  FROM node:24-bookworm-slim AS build
4
4
  ARG SQLITE_PREBUILD_MIRROR
5
5
  ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
6
- ARG HAI_DEPLOY_ENVIRONMENT=prod
7
- ARG HAI_CORE_ENV=production
8
- ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
9
- ENV HAI_CORE_ENV=$HAI_CORE_ENV
10
6
 
11
7
  ENV PNPM_HOME=/pnpm
12
8
  ENV PATH=$PNPM_HOME:$PATH
@@ -14,14 +10,14 @@ ENV CI=true
14
10
  WORKDIR /workspace
15
11
 
16
12
  RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
17
- COPY pnpm-lock.yaml pnpm-workspace.yaml ./
18
- RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch
19
13
  COPY . .
14
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch
20
15
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
21
- RUN pnpm build
16
+ RUN pnpm compile
22
17
 
23
18
  FROM nginxinc/nginx-unprivileged:1.29-alpine AS runtime
24
19
 
20
+ COPY --chmod=755 docker/19-runtime-config.sh /docker-entrypoint.d/19-hai-runtime-config.sh
25
21
  COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
26
22
  COPY --from=build /workspace/dist /usr/share/nginx/html
27
23
 
@@ -32,10 +28,6 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
32
28
  FROM node:24-bookworm-slim AS build
33
29
  ARG SQLITE_PREBUILD_MIRROR
34
30
  ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
35
- ARG HAI_DEPLOY_ENVIRONMENT=prod
36
- ARG HAI_CORE_ENV=production
37
- ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
38
- ENV HAI_CORE_ENV=$HAI_CORE_ENV
39
31
 
40
32
  ENV PNPM_HOME=/pnpm
41
33
  ENV PATH=$PNPM_HOME:$PATH
@@ -43,21 +35,16 @@ ENV CI=true
43
35
  WORKDIR /workspace
44
36
 
45
37
  RUN corepack enable && corepack prepare pnpm@12.11.2 --activate
46
- COPY pnpm-lock.yaml pnpm-workspace.yaml ./
47
- RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch
48
38
  COPY . .
39
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch
49
40
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
50
- RUN pnpm build
41
+ RUN pnpm compile
51
42
  RUN if [ -d node_modules/better-sqlite3 ]; then cd node_modules/better-sqlite3; for attempt in 1 2 3; do node -e "require('node:child_process').execFileSync(process.execPath,[require.resolve('prebuild-install/bin.js'),'--verbose'],{stdio:'inherit'})" && break; [ "$attempt" = 3 ] && exit 1; sleep 3; done; fi
52
43
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{projectName}} --prod /prod/app
53
44
 
54
45
  FROM node:24-bookworm-slim AS runtime-base
55
- ARG HAI_DEPLOY_ENVIRONMENT=prod
56
- ARG HAI_CORE_ENV=production
57
46
 
58
47
  ENV NODE_ENV=production
59
- ENV HAI_DEPLOY_ENVIRONMENT=$HAI_DEPLOY_ENVIRONMENT
60
- ENV HAI_CORE_ENV=$HAI_CORE_ENV
61
48
  ENV HOST=0.0.0.0
62
49
  ENV PORT=3000
63
50
  WORKDIR /app
@@ -71,9 +58,5 @@ HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
71
58
  CMD node -e "fetch('http://127.0.0.1:'+process.env.PORT+'/').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
72
59
  CMD ["node", "build"]
73
60
 
74
- FROM runtime-base AS runtime-with-data
75
-
76
- COPY --from=embedded-data --chown=node:node / /app/data/
77
-
78
61
  FROM runtime-base AS runtime
79
62
  {{/if}}
@@ -9,6 +9,12 @@ server {
9
9
  try_files $uri $uri/ /index.html;
10
10
  }
11
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
+
12
18
  location = /health {
13
19
  access_log off;
14
20
  add_header Content-Type text/plain;
@@ -0,0 +1 @@
1
+ { "images": [{ "name": "app", "target": "runtime", "containerPort": {{#if isSvelteOnlyApp}}8080{{else}}3000{{/if}} }] }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
3
3
  "name": "{{projectName}}",
4
- "version": "0.1.0-alpha.65",
4
+ "version": "0.1.0-alpha.66",
5
5
  "private": true,
6
6
  "packageManager": "pnpm@12.11.2",
7
7
  "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}}",
@@ -23,8 +23,9 @@
23
23
  "dev": "vite dev",
24
24
  "dev:test": "vite dev --mode test",
25
25
  "dev:prod": "vite dev --mode prod",
26
- "build": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vite build",
27
- "build:prod": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vite build --mode prod",
26
+ "compile": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vite build",
27
+ "build": "hai build",
28
+ "deploy": "hai deploy",
28
29
  {{#unless isSvelteOnlyApp}}
29
30
  "start": "node --env-file=.env --env-file=.env.prod --env-file-if-exists=.env.local build/index.js",
30
31
  "start:test": "node --env-file=.env --env-file=.env.test --env-file-if-exists=.env.local build/index.js",
@@ -37,26 +38,20 @@
37
38
  "lint:fix": "eslint . --fix",
38
39
  "test": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vitest run",
39
40
  "test:watch": "vitest",
40
- "test:e2e": "playwright test",
41
- "clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}",
42
- "docker:build": "docker build --target runtime -t {{projectName}}:latest .",
43
- {{#unless isSvelteOnlyApp}}
44
- "docker:build:data": "docker build --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest .",
45
- {{/unless}}
46
- "podman:build": "podman build --format docker --jobs=1 --target runtime -t {{projectName}}:latest ."{{#unless isSvelteOnlyApp}},
47
- "podman:build:data": "podman build --format docker --jobs=1 --target runtime-with-data --build-context embedded-data=./data -t {{projectName}}:latest ."{{/unless}}{{#if isCapacitorApp}},
41
+ "test:e2e": "node scripts/run-e2e.mjs",
42
+ "clean": "{{#if isSvelteOnlyApp}}rimraf dist build android ios .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}"{{#if isCapacitorApp}},
48
43
  "cap:add:android": "cap add android",
49
44
  "cap:add:ios": "cap add ios",
50
- "cap:sync": "pnpm build && cap sync",
51
- "cap:sync:android": "pnpm build && cap sync android",
52
- "cap:sync:ios": "pnpm build && cap sync ios",
45
+ "cap:sync": "pnpm compile && cap sync",
46
+ "cap:sync:android": "pnpm compile && cap sync android",
47
+ "cap:sync:ios": "pnpm compile && cap sync ios",
53
48
  "cap:android": "cap open android",
54
49
  "cap:ios": "cap open ios",
55
50
  "cap:run:android": "cap run android",
56
51
  "cap:run:ios": "cap run ios",
57
- "cap:build:android:debug": "pnpm build && cap sync android && cd android && gradlew.bat assembleDebug",
58
- "cap:build:android:release": "pnpm build && cap sync android && cap build android --androidreleasetype APK",
59
- "cap:build:ios": "pnpm build && cap sync ios && cap build ios"{{/if}}
52
+ "cap:build:android:debug": "pnpm compile && cap sync android && cd android && gradlew.bat assembleDebug",
53
+ "cap:build:android:release": "pnpm compile && cap sync android && cap build android --androidreleasetype APK",
54
+ "cap:build:ios": "pnpm compile && cap sync ios && cap build ios"{{/if}}
60
55
  },
61
56
  "dependencies": {
62
57
  {{#if isSvelteOnlyApp}}
@@ -88,6 +83,7 @@
88
83
  {{/if}}
89
84
  },
90
85
  "devDependencies": {
86
+ "@h-ai/cli": "{{haiPackageSpecifier}}",
91
87
  {{#if isSvelteOnlyApp}}
92
88
  "@h-ai/core": "{{haiPackageSpecifier}}",
93
89
  {{/if}}
@@ -56,6 +56,8 @@ const config = {
56
56
  runes: true,
57
57
  },
58
58
  kit: {
59
+ // 环境文件由 Vite 的 core loader 统一加载;Kit 不再二次读取根目录私有配置。
60
+ env: { dir: './.svelte-kit' },
59
61
  {{#if isCapacitorApp}}
60
62
  adapter: adapter({
61
63
  pages: 'build',
@@ -22,9 +22,12 @@ import { sveltekit } from '@sveltejs/kit/vite'
22
22
  import tailwindcss from '@tailwindcss/vite'
23
23
  {{/if}}
24
24
  import { defineConfig } from 'vite'
25
+ {{#if isSvelteOnlyApp}}
26
+ import { publicRuntimeConfig } from './scripts/vite-runtime-config.js'
27
+ {{/if}}
25
28
 
26
- export default defineConfig(({ mode }) => {
27
- const envResult = core.config.loadEnvFiles({ mode, envDir: process.cwd() })
29
+ export default defineConfig(({ mode, command }) => {
30
+ const envResult = command === 'build' ? { success: true as const, data: {} as Record<string, string> } : core.config.loadEnvFiles({ mode, envDir: process.cwd() })
28
31
  if (!envResult.success)
29
32
  throw new Error(envResult.error.message)
30
33
  const env = envResult.data
@@ -50,6 +53,7 @@ export default defineConfig(({ mode }) => {
50
53
  {{/if}}
51
54
  plugins: [
52
55
  {{#if isSvelteOnlyApp}}
56
+ publicRuntimeConfig({ apiBase: process.env.PUBLIC_API_BASE, core: appConfig }),
53
57
  svelte(),
54
58
  {{else}}
55
59
  sveltekit(),
@@ -0,0 +1,5 @@
1
+ # Windows 构建机交付 Linux 启动脚本时必须保留 LF,避免 /bin/sh 解释器失效。
2
+ *.sh text eol=lf
3
+ *.sh.hbs text eol=lf
4
+ *.envsh text eol=lf
5
+ *.envsh.hbs text eol=lf
@@ -0,0 +1,30 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ # 只编码公开白名单;Base64 不会把环境值中的引号或换行解释为 JavaScript。
5
+ encode() { printf '%s' "$1" | base64 | tr -d '\n'; }
6
+ mkdir -p /tmp/hai-runtime-config
7
+ cat > /tmp/hai-runtime-config/runtime-config.js <<EOF
8
+ {
9
+ const decode = value => new TextDecoder().decode(Uint8Array.from(atob(value), character => character.charCodeAt(0)));
10
+ const nonempty = object => Object.fromEntries(Object.entries(object).filter(([, value]) => value !== ''));
11
+ const core = nonempty({
12
+ name: decode('$(encode "${HAI_CORE_NAME:-}")'),
13
+ version: decode('$(encode "${HAI_CORE_VERSION:-}")'),
14
+ env: decode('$(encode "${HAI_CORE_ENV:-}")'),
15
+ defaultLocale: decode('$(encode "${HAI_CORE_DEFAULTLOCALE:-}")')
16
+ });
17
+ const logging = nonempty({
18
+ level: decode('$(encode "${HAI_CORE_LOGGING_LEVEL:-}")'),
19
+ format: decode('$(encode "${HAI_CORE_LOGGING_FORMAT:-}")')
20
+ });
21
+ if (Object.keys(logging).length) core.logging = logging;
22
+ const config = nonempty({
23
+ apiBase: decode('$(encode "${PUBLIC_API_BASE:-}")'),
24
+ apiTransport: decode('$(encode "${PUBLIC_API_TRANSPORT:-}")'),
25
+ apiKeyExchangePath: decode('$(encode "${PUBLIC_API_KEY_EXCHANGE_PATH:-}")')
26
+ });
27
+ if (Object.keys(core).length) config.core = core;
28
+ globalThis.__HAI_PUBLIC_CONFIG__ = { ...globalThis.__HAI_PUBLIC_CONFIG__, ...config };
29
+ }
30
+ EOF
@@ -0,0 +1,65 @@
1
+ /** 运行浏览器验收,并在报告写完后清理本次输出;失败和中断同样清理。 */
2
+ import { spawn } from 'node:child_process'
3
+ import { mkdirSync, mkdtempSync, rmSync } from 'node:fs'
4
+ import { createRequire } from 'node:module'
5
+ import { tmpdir } from 'node:os'
6
+ import { join, resolve } from 'node:path'
7
+ import process from 'node:process'
8
+
9
+ const cwd = process.cwd()
10
+ const require = createRequire(resolve(cwd, 'package.json'))
11
+ const cli = require.resolve('@playwright/test/cli')
12
+ const dataParent = resolve(process.env.HAI_E2E_DATA_ROOT || tmpdir())
13
+ mkdirSync(dataParent, { recursive: true })
14
+ const dataRoot = mkdtempSync(join(dataParent, 'hai-browser-e2e-'))
15
+ const groups = [[]]
16
+ for (const argument of process.argv.slice(2)) {
17
+ if (argument === '--next')
18
+ groups.push([])
19
+ else
20
+ groups.at(-1).push(argument)
21
+ }
22
+ let child
23
+ let interrupted = false
24
+ function stop() {
25
+ if (interrupted)
26
+ return
27
+ interrupted = true
28
+ if (process.platform === 'win32' && child?.pid && child.exitCode === null) {
29
+ // Windows 的 kill 不传递 SIGTERM;同时结束本次 Playwright 创建的浏览器与服务进程。
30
+ spawn('taskkill', ['/pid', String(child.pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' })
31
+ .once('error', () => child?.kill())
32
+ }
33
+ else {
34
+ child?.kill()
35
+ }
36
+ }
37
+ process.once('SIGINT', stop)
38
+ process.once('SIGTERM', stop)
39
+ try {
40
+ for (const args of groups) {
41
+ const code = await new Promise((resolve, reject) => {
42
+ child = spawn(process.execPath, [cli, 'test', ...args], {
43
+ cwd,
44
+ windowsHide: true,
45
+ stdio: 'inherit',
46
+ env: { ...process.env, HAI_AUTH_EVIDENCE_DIR: join(cwd, 'test-results/evidence'), HAI_E2E_DATA_ROOT: dataRoot },
47
+ })
48
+ child.once('error', reject)
49
+ child.once('exit', resolve)
50
+ })
51
+ if (code !== 0 || interrupted) {
52
+ process.exitCode = typeof code === 'number' && code !== 0 ? code : 1
53
+ break
54
+ }
55
+ }
56
+ }
57
+ finally {
58
+ process.removeListener('SIGINT', stop)
59
+ process.removeListener('SIGTERM', stop)
60
+ // 固定为当前测试工程中的标准输出目录,禁止删除调用方任意指定的路径。
61
+ for (const name of ['test-results', 'playwright-report'])
62
+ rmSync(join(cwd, name), { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
63
+ // Playwright 退出后服务进程已关闭,此时删除 SQLite / storage,避免 Windows 文件锁。
64
+ rmSync(dataRoot, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 })
65
+ }
@@ -0,0 +1,54 @@
1
+ /** Vite 开发与预览的公开运行配置;同一份静态构建可连接不同环境的 API。 */
2
+ import type { CoreConfig } from '@h-ai/core'
3
+ import type { IncomingMessage, ServerResponse } from 'node:http'
4
+ import type { Plugin } from 'vite'
5
+
6
+ /** 只允许浏览器可见的地址、开关和品牌字段,禁止传入 process.env 或完整服务配置。 */
7
+ export interface PublicRuntimeConfig {
8
+ /** 包含 API 前缀的公开地址,例如 https://example.test/api/v1。 */
9
+ apiBase?: string
10
+ /** 客户端传输开关,与服务端一致。 */
11
+ apiTransport?: string
12
+ /** 公开密钥交换端点。 */
13
+ apiKeyExchangePath?: string
14
+ /** 客户端公开元数据;不含密钥或连接参数。 */
15
+ core?: Pick<CoreConfig, 'name' | 'version' | 'env' | 'defaultLocale'> & { logging?: Pick<NonNullable<CoreConfig['logging']>, 'level' | 'format'> }
16
+ }
17
+
18
+ /**
19
+ * 在加载应用前提供无缓存的公开配置;构建只复制空的占位文件。
20
+ * @example `plugins: [publicRuntimeConfig({ apiBase: process.env.PUBLIC_API_BASE })]`
21
+ */
22
+ export function publicRuntimeConfig(config: PublicRuntimeConfig): Plugin {
23
+ const metadata = config.core
24
+ // 明确投影字段,JavaScript 调用方即使误传完整配置也不能公开连接参数或密钥。
25
+ const publicConfig: PublicRuntimeConfig = {
26
+ apiBase: config.apiBase,
27
+ apiTransport: config.apiTransport,
28
+ apiKeyExchangePath: config.apiKeyExchangePath,
29
+ core: metadata
30
+ ? {
31
+ name: metadata.name,
32
+ version: metadata.version,
33
+ env: metadata.env,
34
+ defaultLocale: metadata.defaultLocale,
35
+ logging: metadata.logging ? { level: metadata.logging.level, format: metadata.logging.format } : undefined,
36
+ }
37
+ : undefined,
38
+ }
39
+ const body = `globalThis.__HAI_PUBLIC_CONFIG__ = { ...globalThis.__HAI_PUBLIC_CONFIG__, ...${JSON.stringify(publicConfig)} };\n`
40
+ const respond = (_request: IncomingMessage, response: ServerResponse): void => {
41
+ response.setHeader('Content-Type', 'application/javascript; charset=utf-8')
42
+ response.setHeader('Cache-Control', 'no-store')
43
+ response.end(body)
44
+ }
45
+ return {
46
+ name: 'hai-public-runtime-config',
47
+ configureServer(server) {
48
+ server.middlewares.use('/runtime-config.js', respond)
49
+ },
50
+ configurePreviewServer(server) {
51
+ server.middlewares.use('/runtime-config.js', respond)
52
+ },
53
+ }
54
+ }
@@ -48,7 +48,7 @@ YAML 使用 `${HAI_DB_TYPE:sqlite}` 等显式引用,冒号后是未配置时
48
48
 
49
49
  | 入口 | 开发 | 测试环境 | 生产环境 |
50
50
  | ---------------- | ---------- | ------------------------ | --------------------------------------------- |
51
- | Vite / SvelteKit | `pnpm dev` | `pnpm dev:test` | `pnpm build:prod` 或 `pnpm dev:prod` 预览配置 |
51
+ | Vite / SvelteKit | `pnpm dev` | `pnpm dev:test` | `pnpm dev:prod` 预览;build 不选择环境 |
52
52
  | Node service | `pnpm dev` | 构建后 `pnpm start:test` | 构建后 `pnpm start:prod` |
53
53
 
54
54
  所有入口优先级一致:**进程环境 > `.env.local` > `.env.<dev|test|prod>` > `.env`**。Vite 配置在 Node 阶段通过 `core.config.loadEnvFiles({ mode, envDir })` 读取,检查 `success` 后注入进程;`development`/`production` 分别映射到 `dev`/`prod`。Node 服务按 `.env`、环境文件、`.env.local` 的顺序传入 `--env-file`,后者覆盖前者,已有进程环境最高。`core.init` 只读取已注入的变量,不自行读取 dotenv。
@@ -209,6 +209,8 @@ export function pageOffset(query: PageQuery): number {
209
209
 
210
210
  ## 验证与交付
211
211
 
212
+ 所有测试临时工程、镜像、容器、辅助进程、日志、截图与报告须在 finally/teardown 自动清理,成功、失败和取消均执行;只清理本次创建的资源,不提交验收产物。
213
+
212
214
  - 对新增/改变行为按 [hai-app-tests](../hai-app-tests/SKILL.md) 写有意义的测试;目标用例先因行为缺失失败,既有回归保持通过。UI/路由改动覆盖真实流程、异步失败与权限路径;纯文案/文档按影响验证。
213
215
  - 从当前 `package.json` 选择实际脚本,优先 workspace filter;按 `typecheck → lint → build(构建/配置/跨包变更)→ test → 相关 E2E` 验证,PowerShell 使用 `pnpm.cmd`。不能调用框架仓库私有命令替代应用检查。
214
216
  - typecheck/lint 只覆盖部分质量规则;权限、事务、降级、真实 provider、浏览器、容器和原生交付各自需要适用行为证据。缺环境或密钥时报告未验证,不能用 mock 冒充真实集成。
@@ -334,13 +334,13 @@ core.init() → crypto.init() → reldb.init() → cache.init() → iam.init()
334
334
 
335
335
  仅涉及容器构建/部署变更时使用本节;普通功能开发无需每次启动镜像。
336
336
 
337
- - 保持脚手架根目录 `Dockerfile`、`.dockerignore` 与 `package.json` 中 `docker:build*` / `podman:build*` 命令同步。
337
+ - 保持脚手架根目录 `Dockerfile`、`.dockerignore` 与 `package.json` 中 `build` / `compile` 命令与 hai.build.json同步。
338
338
  - Podman 多阶段构建保持 `--format docker --jobs=1`;前者保留 `HEALTHCHECK`,后者避免部分 Podman 5.8/Buildah 组合在并行 stage 构建时触发内部 semaphore panic。
339
339
  - 先复制 lockfile 与 workspace 配置执行 `pnpm fetch`,再复制源码并离线安装、构建;避免源码变化重复下载依赖,同时兼容新增 workspace。
340
340
  - SvelteKit 服务使用 Node adapter;纯 Vite 前端使用带 SPA fallback 的静态镜像;fullstack 的 Service 与 Web 分别交付,Service 直接监听且不增加转发进程。
341
341
  - Web 的 `PUBLIC_*` 部署配置在容器启动时生成静态运行时配置,不作为 build arg 烘焙,确保同一镜像可跨环境部署。
342
342
  - 配置文件随镜像提供,并允许只读挂载覆盖;密钥只通过环境变量或 `--env-file` 注入。
343
- - 普通构建不携带 `data/`;只有用户明确运行 `docker:build:data` / `podman:build:data` 时才通过 named build context 嵌入 SQLite/本地存储初始快照。
343
+ - 普通构建不携带 `data/`;持续写入使用运行期 volume;需要预置数据时显式维护独立 Dockerfile target 和 named build context,不把本机数据加入默认镜像。
344
344
  - 数据库和本地存储的持续写入必须落到 volume,不能依赖镜像可写层。
345
345
  - 完成前实际启动镜像,验证健康端点、非 root 用户、环境变量覆盖、配置挂载和预置数据读取。
346
346
 
@@ -21,7 +21,22 @@ description: "定位 hai 应用架构、模块依赖、初始化顺序、构建
21
21
  - 升级 hai-framework、比较版本差异或同步框架 skills 时,使用 [hai-framework-sync](../hai-framework-sync/SKILL.md) 完成版本发现、差异分析、选择/阻塞处理、迁移与验证。
22
22
  - 功能开发先用 [development.md](../hai-app-create/development.md) 把需求匹配到当前版本的框架能力与配置;再读取所选模块 skill 和公开类型。
23
23
  - 有生命周期的模块按真实依赖初始化、检查结果并反序清理;core、纯函数和工厂不套用相同生命周期。
24
- - 构建任务检查当前 engines、适配器、环境变量、缓存与目标产物,不能照搬框架仓库私有构建命令。
24
+ - 构建任务检查当前 engines、适配器、缓存与目标产物,不能照搬框架仓库私有构建命令。
25
+
26
+ ## 构建与部署
27
+
28
+ - `pnpm build` 默认仅编译源码(调用 compile);`pnpm build --target docker` 根据 hai.build.json 构建并导出 app/可选 web 镜像,auto 优先可达 Podman、其次 Docker,可用 --runtime 显式指定。
29
+ - compile 交付源码编译结果,docker 交付镜像归档,desktop 交付 Tauri 系统安装包;每次只选一种。images[].target 是 Dockerfile 阶段名,fullstack 的 app 使用 runtime、web 使用 web,不能与 CLI target 混用;参数默认值和示例必须与真实入口同步。
30
+ - Podman 的客户端代理通过预定义构建参数传递,覆盖 Windows/macOS 远程引擎的服务端默认值;代理地址必须能从构建容器访问,--no-http-proxy 禁止此传递。不要把代理写成 Dockerfile ENV 或固化进交付镜像。
31
+ - --network 默认沿用引擎网络;只有实际构建网络无法访问依赖/代理时才显式选择网络,pasta 仅适用于 rootless Podman。build 与 run 的主机名解析可能不同,须验证构建网络中的可达性,不默认改成 host 网络。
32
+ - 从运行 build 的应用根 package.json.version 读取版本,不能使用 CLI/框架版本或服务子包版本代替。清单格式 version=2,appVersion 为应用版本;镜像 tar 名含版本、buildId 区分同版本构建,Tauri 也接收根版本。不要手改清单或保留无版本的交付归档。
33
+ - 构建不区分 dev/test/prod,不加载部署环境覆盖,不注入运行期凭据;完整镜像清单含 SHA-256,全部成功才发布 latest。不要让 compile、Dockerfile 或 Tauri beforeBuildCommand 递归调用自身的原生/镜像构建。
34
+ - Vite 的 envDir 与 SvelteKit 的 kit.env.dir 是独立加载入口,必须同时阻止构建阶段读取工程根的 .env 文件;开发/预览由 core loader 注入,运行期由 deploy 注入。用临时 .env.local 哨兵值检查输出,避免只过滤进程变量却仍把私有文件固化进产物。
35
+ - 静态客户端通过 runtime-config.js 或原生宿主初始化脚本获取公开地址与元数据;仅投影公开白名单,禁止将完整 process.env、服务配置或密钥序列化到浏览器。预览与容器启动均须验证真实登录,不能依赖构建期 API 地址让 E2E 通过。
36
+ - `pnpm build --target desktop --platform windows|linux|macos --arch x64|arm64` 调用对应工程的 Tauri build。要求在目标系统准备 Rust target、SDK 和打包工具,不假定跨系统编译。原生安装包必须按目标系统实测。
37
+ - deploy 只消费已有产物,详见 [hai-deploy](../hai-deploy/SKILL.md)。网络不通时 --bundle-only 生成 tar,按说明手工搬运;packaged 不代表部署已运行。
38
+ - 测试临时工程、镜像、容器、辅助进程、日志、截图和报告必须在 finally/teardown 自动清理,失败/取消也清理;不得提交测试产物或删除用户既有资源。
39
+ - 浏览器测试的数据放入本轮独立临时目录;Windows 等子进程与服务退出后再删除数据库和上传文件,不依赖下一轮测试补清理。
25
40
 
26
41
  ## 按需参考
27
42