@h-ai/cli 1.0.0-beta.2 → 1.0.0-beta.4

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 (58) hide show
  1. package/README.en.md +20 -8
  2. package/README.ja.md +20 -8
  3. package/README.md +20 -8
  4. package/dist/{chunk-MUHRBJSW.js → chunk-K6RWTTHD.js} +546 -97
  5. package/dist/chunk-K6RWTTHD.js.map +1 -0
  6. package/dist/{chunk-AKV2CQZV.js → chunk-SWEU7KUE.js} +71 -59
  7. package/dist/chunk-SWEU7KUE.js.map +1 -0
  8. package/dist/{chunk-EF7WOFWZ.js → chunk-YVNNMC4H.js} +364 -136
  9. package/dist/chunk-YVNNMC4H.js.map +1 -0
  10. package/dist/cli-config-templates-LUJGDQNW.js +5 -0
  11. package/dist/{cli-config-templates-HHFHNXBN.js.map → cli-config-templates-LUJGDQNW.js.map} +1 -1
  12. package/dist/{cli-skill-templates-PN4FLGBC.js → cli-skill-templates-VV2XWYON.js} +3 -3
  13. package/dist/{cli-skill-templates-PN4FLGBC.js.map → cli-skill-templates-VV2XWYON.js.map} +1 -1
  14. package/dist/index.js +117 -208
  15. package/dist/index.js.map +1 -1
  16. package/dist/metafile-esm.json +1 -1
  17. package/dist/{src-TEBHR5I5.js → src-IR5AU43K.js} +3 -3
  18. package/dist/src-IR5AU43K.js.map +1 -0
  19. package/package.json +11 -11
  20. package/templates/apps/api/.dockerignore.hbs +10 -0
  21. package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +20 -1
  22. package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +6 -1
  23. package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +1 -1
  24. package/templates/apps/api/images/serv.Dockerfile.hbs +3 -2
  25. package/templates/apps/fullstack/.dockerignore.hbs +10 -0
  26. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +1 -1
  27. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +16 -2
  28. package/templates/apps/fullstack/images/serv.Dockerfile.hbs +6 -2
  29. package/templates/apps/fullstack/images/web.Dockerfile.hbs +6 -1
  30. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +1 -1
  31. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +10 -7
  32. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +14 -7
  33. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
  34. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +1 -1
  35. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +7 -1
  36. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +1 -1
  37. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +1 -0
  38. package/templates/apps/website/scripts/static-website.mjs.hbs +3 -3
  39. package/templates/base/.dockerignore.hbs +10 -0
  40. package/templates/base/images/web.Dockerfile.hbs +4 -2
  41. package/templates/base/package.json.hbs +1 -1
  42. package/templates/base/src/hooks.server.ts.hbs +9 -7
  43. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +3 -1
  44. package/templates/skills/hai-ai/guide.md +5 -3
  45. package/templates/skills/hai-app-create/development.md +5 -3
  46. package/templates/skills/hai-app-create/reference.md +1 -1
  47. package/templates/skills/hai-build/SKILL.md +5 -5
  48. package/templates/skills/hai-build/paths.md +1 -1
  49. package/templates/skills/hai-build/reference.md +1 -1
  50. package/templates/skills/hai-core/request-access.md +1 -1
  51. package/templates/skills/hai-deploy/SKILL.md +15 -3
  52. package/templates/skills/hai-deploy/middleware.md +4 -2
  53. package/templates/skills/hai-serv/reference.md +5 -8
  54. package/dist/chunk-AKV2CQZV.js.map +0 -1
  55. package/dist/chunk-EF7WOFWZ.js.map +0 -1
  56. package/dist/chunk-MUHRBJSW.js.map +0 -1
  57. package/dist/cli-config-templates-HHFHNXBN.js +0 -5
  58. package/dist/src-TEBHR5I5.js.map +0 -1
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, readDeploymentArtifact, saveDeploymentEnvironment } from './chunk-MUHRBJSW.js';
2
+ export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, readDeploymentArtifact, saveDeploymentEnvironment } from './chunk-K6RWTTHD.js';
3
3
  import './chunk-ZSDEVXZ7.js';
4
- //# sourceMappingURL=src-TEBHR5I5.js.map
5
- //# sourceMappingURL=src-TEBHR5I5.js.map
4
+ //# sourceMappingURL=src-IR5AU43K.js.map
5
+ //# sourceMappingURL=src-IR5AU43K.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"src-IR5AU43K.js"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/cli",
3
3
  "type": "module",
4
- "version": "1.0.0-beta.2",
4
+ "version": "1.0.0-beta.4",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -30,7 +30,7 @@
30
30
  "node": "^22.23.4 || ^24.22.1 || ^26.12.1"
31
31
  },
32
32
  "dependencies": {
33
- "@h-ai/core": "1.0.0-beta.2",
33
+ "@h-ai/core": "1.0.0-beta.4",
34
34
  "cac": "^7.0.1",
35
35
  "chalk": "^6.0.1",
36
36
  "fast-wrap-ansi": "^0.2.2",
@@ -41,15 +41,15 @@
41
41
  "yaml": "^2.9.1"
42
42
  },
43
43
  "devDependencies": {
44
- "@h-ai/api-client": "1.0.0-beta.2",
45
- "@h-ai/cache": "1.0.0-beta.2",
46
- "@h-ai/crypto": "1.0.0-beta.2",
47
- "@h-ai/deploy": "1.0.0-beta.2",
48
- "@h-ai/feedback": "1.0.0-beta.2",
49
- "@h-ai/iam": "1.0.0-beta.2",
50
- "@h-ai/reach": "1.0.0-beta.2",
51
- "@h-ai/reldb": "1.0.0-beta.2",
52
- "@h-ai/storage": "1.0.0-beta.2",
44
+ "@h-ai/api-client": "1.0.0-beta.4",
45
+ "@h-ai/cache": "1.0.0-beta.4",
46
+ "@h-ai/crypto": "1.0.0-beta.4",
47
+ "@h-ai/deploy": "1.0.0-beta.4",
48
+ "@h-ai/feedback": "1.0.0-beta.4",
49
+ "@h-ai/iam": "1.0.0-beta.4",
50
+ "@h-ai/reach": "1.0.0-beta.4",
51
+ "@h-ai/reldb": "1.0.0-beta.4",
52
+ "@h-ai/storage": "1.0.0-beta.4",
53
53
  "@types/fs-extra": "^11.0.4",
54
54
  "@types/node": "^26.6.5",
55
55
  "@types/prompts": "^2.4.9",
@@ -23,3 +23,13 @@ tmp
23
23
  # 仅保留公共基础 .env;分环境配置、私有覆盖及已有交付包不进入构建上下文。
24
24
  **/.env.*
25
25
  **/.hai
26
+ # 部署证书和测试材料不进入镜像构建上下文。
27
+ **/*.pem
28
+ **/*.key
29
+ **/*.p12
30
+ **/*.pfx
31
+ **/tests
32
+ **/e2e
33
+ **/*.test.*
34
+ **/*.spec.*
35
+ docs
@@ -7,15 +7,29 @@ server:
7
7
  port: 3000
8
8
 
9
9
  http:
10
+ # auto 仅在受管私有 Compose 代理中采用容器网段;普通宿主忽略转发头。
11
+ trustedProxies: auto
12
+ access: false
13
+ # 独立文件与下方内联规则二选一,用所选配置块替换 access: false。
14
+ # access:
15
+ # file: ./config/security/request-access.yml
16
+ # access:
17
+ # global:
18
+ # timezone: UTC # 可用 HAI_SERV_HTTP_ACCESS_GLOBAL_TIMEZONE=Asia/Shanghai
19
+ # allowIps: []
20
+ # denyIps: []
21
+ # windows: []
22
+ # rules: []
10
23
  apiPrefix: /api/v1
11
24
  openapi:
12
25
  path: /openapi.json
26
+ # HAI_SERV_HTTP_DOCS=false 关闭文档;开启时再配置 path/requireAuth。
13
27
  docs:
14
28
  path: /docs
29
+ # requireAuth: true
15
30
  health:
16
31
  path: /health
17
32
  readyPath: /ready
18
- rpc: false
19
33
 
20
34
  cors:
21
35
  origin: ${HAI_SERV_CORS_ORIGIN:*}
@@ -43,6 +57,11 @@ cors:
43
57
  {{else}}
44
58
  maxPlaintextBytes: ${HAI_SERV_TRANSPORT_MAXPLAINTEXTBYTES:1048576}
45
59
  {{/if}}
60
+ # HAI_SERV_TRANSPORT_ROUTEPLAINTEXTBYTES 可填 JSON 路径映射。
61
+ routePlaintextBytes: {}
62
+ # 单条路径需要更大请求体时填写字节数,* 只匹配一段路径。
63
+ # routePlaintextBytes:
64
+ # /api/v1/uploads/*: 8388608
46
65
  keyExchangePath: /_hai/key-exchange
47
66
  excludePaths:
48
67
  - /health
@@ -57,7 +57,12 @@ export function {{api.serviceAppFactoryName}}(_options: CreateApiServiceAppOptio
57
57
  },
58
58
  {{else}} routePlaintextBytes: servConfig.transport.routePlaintextBytes,
59
59
  {{/if}}
60
- excludePaths: [...servConfig.transport.excludePaths],
60
+ excludePaths: [...new Set([
61
+ ...servConfig.transport.excludePaths,
62
+ ...(servConfig.http.health ? [servConfig.http.health.path, servConfig.http.health.readyPath].filter((path): path is `/${string}` => Boolean(path)) : []),
63
+ ...(servConfig.http.openapi && security.exposeApiDocs ? [servConfig.http.openapi.path] : []),
64
+ ...(servConfig.http.docs && security.exposeApiDocs ? [servConfig.http.docs.path, `${servConfig.http.docs.path.slice(0, servConfig.http.docs.path.lastIndexOf('/') + 1)}_hai/scalar.js`] : []),
65
+ ])],
61
66
  }
62
67
  }
63
68
  {{else}} const transport = undefined
@@ -24,12 +24,12 @@ import { reach, ReachConfigSchema } from '@h-ai/reach'
24
24
  {{#if features.db}}
25
25
  import { reldb, ReldbConfigSchema } from '@h-ai/reldb'
26
26
  {{/if}}
27
- import { ServConfigSchema } from '@h-ai/serv'
28
27
  {{#if features.audit}}
29
28
  {{#if features.scheduler}}
30
29
  import { scheduler, SchedulerConfigSchema } from '@h-ai/scheduler'
31
30
  {{/if}}
32
31
  {{/if}}
32
+ import { ServConfigSchema } from '@h-ai/serv'
33
33
  {{#if features.storage}}
34
34
  import { storage, StorageConfigSchema } from '@h-ai/storage'
35
35
  {{/if}}
@@ -12,13 +12,14 @@ RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config s
12
12
  COPY pnpm-lock.yaml pnpm-workspace.yaml ./
13
13
  COPY images/dependencies/ ./
14
14
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
15
- COPY . .
16
15
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
16
+ COPY . .
17
17
  ARG HAI_BUILD_ID=
18
18
  ARG HAI_BUILD_COMMIT=unknown
19
19
  RUN HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" pnpm compile
20
20
  RUN --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds 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
21
21
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds pnpm pm deploy --filter {{api.servPackageName}} --prod /prod/app
22
+ RUN rm -f /prod/app/README* && find /prod/app/dist -type f \( -name '*.map' -o -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \) -delete
22
23
 
23
24
  FROM node:24.22.1-bookworm-slim AS runtime-base
24
25
 
@@ -33,5 +34,5 @@ RUN mkdir -p /app/data && chown node:node /app/data
33
34
  USER node
34
35
  EXPOSE 3000
35
36
  HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
36
- 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))"
37
+ CMD node -e "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config' }); const { server, http } = ServConfigSchema.parse(core.config.getOrThrow('serv')); if (http.health === false) process.exit(0); const response = await fetch('http://127.0.0.1:' + server.port + http.health.path, { signal: AbortSignal.timeout(4000) }); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); } })()"
37
38
  CMD ["node", "dist/index.js"]
@@ -23,3 +23,13 @@ tmp
23
23
  # 仅保留公共基础 .env;分环境配置、私有覆盖及已有交付包不进入构建上下文。
24
24
  **/.env.*
25
25
  **/.hai
26
+ # 部署证书和测试材料不进入镜像构建上下文。
27
+ **/*.pem
28
+ **/*.key
29
+ **/*.p12
30
+ **/*.pfx
31
+ **/tests
32
+ **/e2e
33
+ **/*.test.*
34
+ **/*.spec.*
35
+ docs
@@ -32,7 +32,7 @@ pnpm --filter {{projectName}}-web dev
32
32
  pnpm --filter {{projectName}}-web build
33
33
  ```
34
34
 
35
- 从工程根运行 `pnpm build --target docker` 导出镜像。默认带部署工具,可运行 `pnpm run deploy --env test --build` 一次完成构建发布,或 `pnpm run deploy --env prod --bundle-only` 交付已有镜像及跨平台脚本。完整包包含自建中间件,人工部署与按服务更新见工程根 README。
35
+ 从工程根运行 `pnpm build --target image` 导出镜像。默认带部署工具,可运行 `pnpm run deploy --env test --build` 一次完成构建发布,或 `pnpm run deploy --env prod --bundle-only` 交付已有镜像及跨平台脚本。完整包包含自建中间件,人工部署与按服务更新见工程根 README。
36
36
 
37
37
  ### IAM 认证配置
38
38
 
@@ -805,6 +805,8 @@ test('administrator and user complete account, avatar, preference, password and
805
805
  {{#if fullstack.e2eFrontend}}
806
806
 
807
807
  test('feedback submits an empty description and business operator can process and reopen it', async ({ page }, testInfo) => {
808
+ // 附件脱敏、桌面/手机截图和导出校验共用完整流程预算,目录代理也执行相同断言。
809
+ test.setTimeout(120_000)
808
810
  await page.goto('./')
809
811
  await login(page, 'business_operator', businessOperatorPassword)
810
812
  const feedbackImage = Buffer.from(await page.evaluate(() => {
@@ -887,6 +889,11 @@ test('feedback submits an empty description and business operator can process an
887
889
  await expectUncovered(uploaded)
888
890
  await capture(page, 'feedback-preview.png', testInfo.outputPath('feedback-preview.png'))
889
891
  await uploaded.screenshot({ path: testInfo.outputPath('feedback-image-after-mask-detail.png'), animations: 'disabled' })
892
+ // 回执只表示记录已受理;等这份大附件真正上传成功,再开始后台处理操作。
893
+ const largeUpload = page.waitForResponse(response => response.request().method() === 'POST'
894
+ && /\/feedback\/reports\/[^/]+\/attachments\/[^/]+$/.test(new URL(response.url()).pathname)
895
+ && (response.request().postDataBuffer()?.byteLength ?? 0) > 2 * 1024 * 1024, { timeout: 60_000 })
896
+ .then(response => ({ response }), (error: unknown) => ({ error }))
890
897
  await dialog.getByRole('button', { name: /提交反馈|Submit Feedback|フィードバックを送信/ }).click()
891
898
  await expect(dialog.getByTestId('feedback-receipt')).toContainText(/FB-/)
892
899
  const number = (await dialog.getByTestId('feedback-receipt').textContent())?.match(/FB-[A-Za-z0-9-]+/)?.[0]
@@ -895,13 +902,20 @@ test('feedback submits an empty description and business operator can process an
895
902
  throw new Error('Feedback receipt number is missing')
896
903
  await expectDialogViewport()
897
904
  await capture(page, 'feedback-accepted.png', testInfo.outputPath('feedback-accepted.png'))
905
+ const completedUpload = await largeUpload
906
+ if ('error' in completedUpload)
907
+ throw completedUpload.error
908
+ expect(completedUpload.response.status()).toBe(200)
898
909
  await dialog.getByRole('button', { name: /关闭|Close|閉じる/ }).filter({ hasText: /关闭|Close|閉じる/ }).click()
899
910
  await page.getByTestId('nav-feedback').click()
900
911
  await expect(page.getByTestId('feedback-page')).toBeVisible()
901
912
  const row = page.getByRole('row').filter({ hasText: number })
902
913
  await expect(row).toBeVisible()
903
- await row.getByRole('button', { name: /标记已处理|Mark handled|処理済み/ }).click()
904
- await expect(row).toContainText(/已处理|Handled|処理済み/)
914
+ const handled = page.waitForResponse(response => response.request().method() === 'PATCH' && new URL(response.url()).pathname.endsWith('/status'))
915
+ await row.getByRole('button', { name: /标记已处理|Mark(?: as)? handled|処理済み/ }).click()
916
+ expect((await handled).status()).toBe(200)
917
+ // 仅核对状态徽标;“标记已处理”按钮本身也包含“已处理”,不能据此放行。
918
+ await expect(row.getByText(/^(已处理|Handled|処理済み)$/)).toBeVisible()
905
919
  await row.getByRole('button', { name: /撤销处理|Reopen|処理を取り消す/ }).scrollIntoViewIfNeeded()
906
920
  await capture(page, 'feedback-handled.png', testInfo.outputPath('feedback-handled.png'))
907
921
  await row.getByRole('button', { name: /撤销处理|Reopen|処理を取り消す/ }).click()
@@ -12,14 +12,18 @@ RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config s
12
12
  COPY pnpm-lock.yaml pnpm-workspace.yaml ./
13
13
  COPY images/dependencies/ ./
14
14
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
15
- COPY . .
16
15
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
16
+ COPY tsconfig.base.json ./
17
+ COPY scripts/ ./scripts/
18
+ COPY packages/{{projectName}}-contract/ ./packages/{{projectName}}-contract/
19
+ COPY packages/{{projectName}}-serv/ ./packages/{{projectName}}-serv/
17
20
  ARG HAI_BUILD_ID=
18
21
  ARG HAI_BUILD_COMMIT=unknown
19
22
  RUN HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" pnpm --filter {{fullstack.servPackageName}}... --if-present build
20
23
 
21
24
  RUN --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds 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
22
25
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
26
+ RUN rm -f /prod/service/README* && find /prod/service/dist -type f \( -name '*.map' -o -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \) -delete
23
27
 
24
28
  FROM node:24.22.1-bookworm-slim AS service
25
29
 
@@ -34,5 +38,5 @@ RUN mkdir -p /app/data && chown node:node /app/data
34
38
  USER node
35
39
  EXPOSE 3000
36
40
  HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
37
- 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))"
41
+ CMD node -e "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config' }); const { server, http } = ServConfigSchema.parse(core.config.getOrThrow('serv')); if (http.health === false) process.exit(0); const response = await fetch('http://127.0.0.1:' + server.port + http.health.path, { signal: AbortSignal.timeout(4000) }); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); } })()"
38
42
  CMD ["node", "dist/index.js"]
@@ -12,8 +12,12 @@ RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config s
12
12
  COPY pnpm-lock.yaml pnpm-workspace.yaml ./
13
13
  COPY images/dependencies/ ./
14
14
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
15
- COPY . .
16
15
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
16
+ COPY tsconfig.base.json ./
17
+ COPY scripts/ ./scripts/
18
+ COPY packages/{{projectName}}-contract/ ./packages/{{projectName}}-contract/
19
+ COPY packages/{{projectName}}-shared/ ./packages/{{projectName}}-shared/
20
+ COPY apps/{{projectName}}-web/ ./apps/{{projectName}}-web/
17
21
  ARG HAI_BUILD_ID=
18
22
  ARG HAI_BUILD_COMMIT=unknown
19
23
  ARG PUBLIC_BASE_PATH=
@@ -30,6 +34,7 @@ COPY images/web/nginx.conf /etc/nginx/conf.d/default.conf
30
34
  COPY --chmod=755 images/web/19-runtime-config.envsh /docker-entrypoint.d/19-runtime-config.envsh
31
35
  COPY images/web/runtime-config.js.template /etc/nginx/templates/runtime-config.js.template
32
36
  COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
37
+ RUN find /usr/share/nginx/html -type f -name '*.map' -delete
33
38
 
34
39
  EXPOSE 8080
35
40
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
@@ -48,7 +48,7 @@ CLI 同时在根 `.env.dev/.env.test/.env.prod` 分别生成独立的 `HAI_CRYPT
48
48
 
49
49
  ### 容器运行
50
50
 
51
- 从工程根运行 `pnpm build --target docker` 导出镜像。默认带部署工具,可运行 `pnpm run deploy --env test --build` 一次完成构建发布,或 `pnpm run deploy --env prod --bundle-only` 交付已有镜像及跨平台脚本。完整包包含自建中间件,人工部署与按服务更新见工程根 README。
51
+ 从工程根运行 `pnpm build --target image` 导出镜像。默认带部署工具,可运行 `pnpm run deploy --env test --build` 一次完成构建发布,或 `pnpm run deploy --env prod --bundle-only` 交付已有镜像及跨平台脚本。完整包包含自建中间件,人工部署与按服务更新见工程根 README。
52
52
 
53
53
  - 服务镜像直接由 `@h-ai/serv` 对外监听,配置挂载点为 `/app/config`,数据目录为 `/app/data`,不包含额外转发进程。
54
54
  - Web 镜像不烘焙 API 地址;启动时使用 `-e PUBLIC_API_BASE=https://api.example.com` 注入,同一镜像可部署到不同环境。未设置时 Web 默认请求自身 origin。
@@ -4,7 +4,10 @@ providers:
4
4
  type: console
5
5
  - name: sms
6
6
  type: console
7
- # SMTP 替换 email 控制台 Provider;同名 Provider 只保留一个。
7
+ # 生产示例:用下面 providers 整段替换上方控制台列表;保留需要的项。
8
+ # 数组索引从 0 开始,删掉某项后同步修改其余环境变量中的索引。
9
+ # providers:
10
+ # SMTP 邮件;名称 email 与下方通知模板对应。
8
11
  # - name: email
9
12
  # type: smtp
10
13
  # host: ${HAI_REACH_PROVIDERS_0_HOST:smtp.example.com}
@@ -16,8 +19,8 @@ providers:
16
19
  # 阿里云短信替换 sms 控制台 Provider。
17
20
  # - name: sms
18
21
  # type: aliyun-sms
19
- # accessKeyId: ${HAI_REACH_PROVIDERS_0_ACCESSKEYID}
20
- # accessKeySecret: ${HAI_REACH_PROVIDERS_0_ACCESSKEYSECRET}
22
+ # accessKeyId: ${HAI_REACH_PROVIDERS_1_ACCESSKEYID}
23
+ # accessKeySecret: ${HAI_REACH_PROVIDERS_1_ACCESSKEYSECRET}
21
24
  # signName: MyApp
22
25
  # endpoint: dysmsapi.aliyuncs.com
23
26
  # 自有发送服务:按需将 template.provider 改为 custom。
@@ -26,9 +29,9 @@ providers:
26
29
  # url: https://notify.example.com/send
27
30
  # method: POST # POST | PUT
28
31
  # headers:
29
- # Authorization: '${HAI_REACH_PROVIDERS_0_HEADERS_AUTHORIZATION}'
32
+ # Authorization: '${HAI_REACH_PROVIDERS_2_HEADERS_AUTHORIZATION}'
30
33
  # timeout: 10000
31
- # idempotent: ${HAI_REACH_PROVIDERS_0_IDEMPOTENT:false} # 仅接收端具备原子持久去重时启用;默认在途不明结果转 unknown
34
+ # idempotent: ${HAI_REACH_PROVIDERS_2_IDEMPOTENT:false} # 仅接收端具备原子持久去重时启用;默认在途不明结果转 unknown
32
35
  templates:
33
36
  - name: otp_email
34
37
  provider: email
@@ -52,8 +55,8 @@ templates:
52
55
  # start: '22:00'
53
56
  # end: '08:00'
54
57
 
55
- # 延迟队列策略随每条消息保存;最多领取 5 次,指数退避 1 分钟至 1 小时。
56
- # 超限 dead 只经可信服务端人工恢复;unknown 不允许自动重发。
58
+ # 消息发送失败后最多尝试 5 次。首次重试等待 1 分钟,以后每次加倍,最长等待 1 小时。
59
+ # 尝试次数用完后停止,交给管理员处理;若无法确认对方是否已收到,先核对结果,避免重复发送。
57
60
  queue:
58
61
  maxAttempts: ${HAI_REACH_QUEUE_MAXATTEMPTS:5}
59
62
  retryDelayMs: ${HAI_REACH_QUEUE_RETRYDELAYMS:60000}
@@ -16,22 +16,24 @@ http:
16
16
  access: false
17
17
  # access:
18
18
  # file: ./config/security/request-access.yml
19
+ # 或将 access: false 替换为下列内联配置;与 file 二选一。
20
+ # access:
21
+ # global:
22
+ # timezone: UTC # 可用 HAI_SERV_HTTP_ACCESS_GLOBAL_TIMEZONE=Asia/Shanghai
23
+ # allowIps: []
24
+ # denyIps: []
25
+ # windows: []
26
+ # rules: []
19
27
  apiPrefix: /api/v1
20
28
  openapi:
21
29
  path: /openapi.json
30
+ # HAI_SERV_HTTP_DOCS=false 关闭文档;开启时再配置 path/requireAuth。
22
31
  docs:
23
32
  path: /docs
24
33
  # requireAuth: true
25
34
  health:
26
35
  path: /health
27
36
  readyPath: /ready
28
- rpc: false
29
- # 内部 RPC 可选;启用时用对象替换 false,gateway-only 必须配置网关密钥。
30
- # rpc:
31
- # prefix: /api/rpc
32
- # access: gateway-only # loopback | private-network | gateway-only
33
- # gatewayHeader: X-Gateway-Key
34
- # gatewaySecret: ${HAI_SERV_HTTP_RPC_GATEWAYSECRET}
35
37
 
36
38
  cors:
37
39
  origin: ${HAI_SERV_CORS_ORIGIN:*}
@@ -60,5 +62,10 @@ transport:
60
62
  {{else}}
61
63
  maxPlaintextBytes: ${HAI_SERV_TRANSPORT_MAXPLAINTEXTBYTES:1048576}
62
64
  {{/if}}
65
+ # HAI_SERV_TRANSPORT_ROUTEPLAINTEXTBYTES 可填 JSON,例如 {"/api/v1/uploads/*":8388608}。
66
+ routePlaintextBytes: {}
63
67
  keyExchangePath: /_hai/key-exchange
64
68
  # excludePaths: [/health, /ready]
69
+ # 单条路径需要更大请求体时填写字节数,* 只匹配一段路径。
70
+ # routePlaintextBytes: # HAI_SERV_TRANSPORT_ROUTEPLAINTEXTBYTES 可填 JSON 路径映射
71
+ # /api/v1/uploads/*: 8388608
@@ -43,11 +43,17 @@
43
43
  "@h-ai/cache": "{{haiPackageSpecifier}}",
44
44
  "@h-ai/core": "{{haiPackageSpecifier}}",
45
45
  "@h-ai/crypto": "{{haiPackageSpecifier}}",
46
+ {{#if features.datapipe}}
47
+ "@h-ai/datapipe": "{{haiPackageSpecifier}}",
48
+ {{/if}}
46
49
  {{#if features.feedback}}
47
50
  "@h-ai/api-contract": "{{haiPackageSpecifier}}",
48
51
  "@h-ai/feedback": "{{haiPackageSpecifier}}",
49
52
  {{/if}}
50
53
  "@h-ai/iam": "{{haiPackageSpecifier}}",
54
+ {{#if features.payment}}
55
+ "@h-ai/payment": "{{haiPackageSpecifier}}",
56
+ {{/if}}
51
57
  "@h-ai/reach": "{{haiPackageSpecifier}}",
52
58
  "@h-ai/reldb": "{{haiPackageSpecifier}}",
53
59
  "@h-ai/scheduler": "{{haiPackageSpecifier}}",
@@ -37,7 +37,7 @@ export function createServerApp(): ServHttpApp {
37
37
  `${servConfig.http.apiPrefix}/assets`,
38
38
  ...(servConfig.http.health ? [servConfig.http.health.path, servConfig.http.health.readyPath].filter((path): path is `/${string}` => Boolean(path)) : []),
39
39
  ...(servConfig.http.openapi && security.exposeApiDocs ? [servConfig.http.openapi.path] : []),
40
- ...(servConfig.http.docs && security.exposeApiDocs ? [servConfig.http.docs.path] : []),
40
+ ...(servConfig.http.docs && security.exposeApiDocs ? [servConfig.http.docs.path, `${servConfig.http.docs.path.slice(0, servConfig.http.docs.path.lastIndexOf('/') + 1)}_hai/scalar.js`] : []),
41
41
  ]
42
42
  {{#if features.feedback}}
43
43
  // 普通业务日志与反馈使用同一可信应用范围;身份仍由 IAM 验证。
@@ -231,7 +231,7 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
231
231
  }
232
232
  })
233
233
 
234
- it('updates personal preferences, validates images and serves only allowed avatar resources', async () => {
234
+ it('updates personal preferences and clears the email address', async () => {
235
235
  expect((await call('/profile/update', { displayName: 'Updated', locale: 'en-US', theme: '{{fullstack.theme.defaultTheme}}' }, userToken)).success).toBe(true)
236
236
  const current = await call<{ metadata: { locale: string, theme: string } }>('/auth/me', undefined, userToken, 'GET')
237
237
  expect(current.success).toBe(true)
@@ -242,6 +242,9 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
242
242
  expect(cleared.success).toBe(true)
243
243
  if (cleared.success)
244
244
  expect(cleared.data.email).toBe('')
245
+ })
246
+
247
+ it('rejects invalid images, oversized payloads and anonymous avatar uploads', async () => {
245
248
  const invalid = await call('/profile/avatar', { contentType: 'image/png', base64: 'PHN2Zz48L3N2Zz4=' }, userToken)
246
249
  expect(invalid.success).toBe(false)
247
250
  if (!invalid.success)
@@ -252,6 +255,9 @@ describe('{{fullstack.servPackageName}} mini administration', () => {
252
255
  expect(anonymous.success).toBe(false)
253
256
  if (!anonymous.success)
254
257
  expect(anonymous.error.httpStatus).toBe(401)
258
+ })
259
+
260
+ it('serves only uploaded avatar resources and restricts profile fields', async () => {
255
261
  const injected = await transport.encryptedFetch(`http://localhost${PREFIX}/profile/update`, {
256
262
  method: 'POST',
257
263
  headers: { 'content-type': 'application/json', 'authorization': `Bearer ${userToken}` },
@@ -58,7 +58,7 @@ beforeAll(async () => {
58
58
  contract: {{fullstack.contractExportName}},
59
59
  procedures: createServerProcedures({ app: { name: 'Security fixture', version: 'test', env: 'test', defaultLocale: 'en-US', supportedLocales: ['en-US'], startedAt: new Date().toISOString(), frontends: ['web'] } }),
60
60
  iam,
61
- http: { openapi: false, docs: false, rpc: false },
61
+ http: { openapi: false, docs: false },
62
62
  })
63
63
  server = serv.listen(app, { host: '127.0.0.1', port: 0 })
64
64
  if (!server.server.listening)
@@ -9,6 +9,7 @@ trustPolicyExclude:
9
9
  # pnpm 12 配置;原生依赖仅运行明确列出的安装脚本。
10
10
  nodeLinker: hoisted
11
11
  allowBuilds:
12
+ onnxruntime-node: true
12
13
  protobufjs: true
13
14
  vue-demi: true
14
15
  better-sqlite3: true
@@ -9,21 +9,21 @@ const message = core.i18n.createMessageGetter({
9
9
  'zh-CN': {
10
10
  flag: 'HAI_BUILD_STATIC仅支持0或1;省略或0保持SSR,1启用website静态构建。',
11
11
  base: 'PUBLIC_BASE_PATH须为空或以/开头的站内目录;不允许外部地址、末尾/、查询、片段或路径逃逸。',
12
- modules: '静态website不能启用服务端业务模块。请使用--features none创建纯展示站点,或继续使用默认SSR。',
12
+ modules: '静态website不能启用服务端业务模块。请移除服务端模块及其初始化后再构建,或继续使用默认SSR。',
13
13
  routes: '静态website不能包含自定义API、表单actions或未预渲染页面;请移除动态入口或使用SSR。',
14
14
  assets: 'static是公开资产目录,不能放入.env私有环境文件或符号链接;请移动私有内容后重新构建。',
15
15
  },
16
16
  'en-US': {
17
17
  flag: 'HAI_BUILD_STATIC accepts only 0 or 1: omitted/0 keeps SSR, and 1 enables a static website build.',
18
18
  base: 'PUBLIC_BASE_PATH must be empty or a site-local directory beginning with /. External URLs, trailing /, queries, fragments and path traversal are not allowed.',
19
- modules: 'A static website cannot enable server-side business modules. Create a presentation site with --features none, or keep the default SSR mode.',
19
+ modules: 'A static website cannot enable server-side business modules. Remove server modules and their initialization before building, or keep the default SSR mode.',
20
20
  routes: 'A static website cannot contain custom APIs, form actions or pages that were not prerendered. Remove dynamic routes or use SSR.',
21
21
  assets: 'static is a public asset directory and cannot contain private .env files or symbolic links. Move private content before building.',
22
22
  },
23
23
  'ja-JP': {
24
24
  flag: 'HAI_BUILD_STATICは0または1です。省略/0はSSR、1はwebsiteの静的構築を有効にします。',
25
25
  base: 'PUBLIC_BASE_PATHは空、または/で始まるサイト内ディレクトリーです。外部URL、末尾の/、クエリー、フラグメント、パス逸脱は使えません。',
26
- modules: '静的websiteではサーバー業務モジュールを有効にできません。--features noneで表示専用サイトを作成するか、既定のSSRを使用してください。',
26
+ modules: '静的websiteではサーバー業務モジュールを有効にできません。サーバーモジュールと初期化処理を除去するか、既定のSSRを使用してください。',
27
27
  routes: '静的websiteに独自API、form actions、事前描画されないページを含められません。動的ルートを削除するかSSRを使用してください。',
28
28
  assets: 'staticは公開資産ディレクトリーです。非公開.envファイルやシンボリックリンクを置かず、非公開内容を移動してから構築してください。',
29
29
  },
@@ -25,3 +25,13 @@ ios
25
25
  # 仅保留公共基础 .env;分环境配置、私有覆盖及已有交付包不进入构建上下文。
26
26
  **/.env.*
27
27
  **/.hai
28
+ # 部署证书和测试材料不进入镜像构建上下文。
29
+ **/*.pem
30
+ **/*.key
31
+ **/*.p12
32
+ **/*.pfx
33
+ **/tests
34
+ **/e2e
35
+ **/*.test.*
36
+ **/*.spec.*
37
+ docs
@@ -11,8 +11,8 @@ RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config s
11
11
  COPY pnpm-lock.yaml pnpm-workspace.yaml ./
12
12
  COPY images/dependencies/ ./
13
13
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
14
- COPY . .
15
14
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
15
+ COPY . .
16
16
  ARG HAI_BUILD_ID=
17
17
  ARG HAI_BUILD_COMMIT=unknown
18
18
  ARG PUBLIC_BASE_PATH=
@@ -23,6 +23,7 @@ FROM nginxinc/nginx-unprivileged:1.29.8-alpine AS runtime
23
23
  COPY --chmod=755 images/web/19-runtime-config.sh /docker-entrypoint.d/19-hai-runtime-config.sh
24
24
  COPY images/web/nginx.conf /etc/nginx/conf.d/default.conf
25
25
  COPY --from=build /workspace/dist /usr/share/nginx/html
26
+ RUN find /usr/share/nginx/html -type f -name '*.map' -delete
26
27
 
27
28
  EXPOSE 8080
28
29
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
@@ -42,14 +43,15 @@ RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config s
42
43
  COPY pnpm-lock.yaml pnpm-workspace.yaml ./
43
44
  COPY images/dependencies/ ./
44
45
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
45
- COPY . .
46
46
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
47
+ COPY . .
47
48
  ARG HAI_BUILD_ID=
48
49
  ARG HAI_BUILD_COMMIT=unknown
49
50
  ARG PUBLIC_BASE_PATH=
50
51
  RUN HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" PUBLIC_BASE_PATH="$PUBLIC_BASE_PATH" pnpm compile
51
52
  RUN --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds 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
53
  RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store --mount=type=cache,id={{projectName}}-sqlite,target=/root/.npm/_prebuilds pnpm pm deploy --filter {{projectName}} --prod /prod/app
54
+ RUN rm -f /prod/app/README* && find /prod/app/build -type f \( -name '*.map' -o -name '*.d.ts' -o -name '*.d.mts' -o -name '*.d.cts' \) -delete
53
55
 
54
56
  FROM node:24.22.1-bookworm-slim AS runtime-base
55
57
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "engines": { "node": "^22.23.4 || ^24.22.1 || ^26.12.1" },
3
3
  "name": "{{projectName}}",
4
- "version": "1.0.0-beta.2",
4
+ "version": "1.0.0-beta.4",
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}}",
@@ -8,13 +8,7 @@
8
8
  import type { TransportEncryptionOptions } from '@h-ai/kit'
9
9
  {{/if}}
10
10
  import type { Handle } from '@sveltejs/kit'
11
- {{#if features.cache}}
12
- import process from 'node:process'
13
- {{else}}
14
- {{#if features.crypto}}
15
11
  import process from 'node:process'
16
- {{/if}}
17
- {{/if}}
18
12
  {{#if hasI18n}}
19
13
  import { base } from '$app/paths'
20
14
  {{else}}
@@ -29,7 +23,7 @@ import { base } from '$app/paths'
29
23
  {{#if hasI18n}}
30
24
  import { paraglideMiddleware } from '$lib/paraglide/server.js'
31
25
  {{/if}}
32
- import { initApp } from '$lib/server/init.js'
26
+ import { closeApp, initApp } from '$lib/server/init.js'
33
27
  import { getKitConfig } from '$lib/server/kit-config.js'
34
28
  {{#if features.cache}}
35
29
  import { cache } from '@h-ai/cache'
@@ -54,6 +48,14 @@ import { reldb } from '@h-ai/reldb'
54
48
 
55
49
  let appInitPromise: Promise<void> | null = null
56
50
 
51
+ // Node 适配器停止接收请求后释放模块,避免数据库或后台任务阻止进程退出。
52
+ process.once('sveltekit:shutdown', () => {
53
+ void closeApp().catch((error: unknown) => {
54
+ core.logger.error('Application shutdown failed', { error })
55
+ process.exitCode = 1
56
+ })
57
+ })
58
+
57
59
  async function ensureAppInitialized() {
58
60
  if (!appInitPromise) {
59
61
  appInitPromise = initApp().catch((err: unknown) => {
@@ -28,5 +28,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
28
28
  metadata,
29
29
  notifyUrl: notifyUrl ?? `${new URL(request.url).origin}${base}/api/v1/payment/notify/${provider}`,
30
30
  })
31
- return kit.response.fromResult(result, locals.requestId)
31
+ if (!result.success)
32
+ return kit.response.fromError(result.error, locals.requestId)
33
+ return kit.response.ok(result.data, locals.requestId)
32
34
  }