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

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 (254) hide show
  1. package/README.en.md +74 -154
  2. package/README.ja.md +74 -154
  3. package/README.md +74 -154
  4. package/dist/{chunk-AKV2CQZV.js → chunk-2ZL5ORQ2.js} +316 -97
  5. package/dist/chunk-2ZL5ORQ2.js.map +1 -0
  6. package/dist/{chunk-EF7WOFWZ.js → chunk-L53P43B2.js} +1312 -163
  7. package/dist/chunk-L53P43B2.js.map +1 -0
  8. package/dist/cli-config-templates-LWMD2I6V.js +5 -0
  9. package/dist/{cli-config-templates-HHFHNXBN.js.map → cli-config-templates-LWMD2I6V.js.map} +1 -1
  10. package/dist/{cli-skill-templates-PN4FLGBC.js → cli-skill-templates-D6S2OQ6X.js} +3 -3
  11. package/dist/{cli-skill-templates-PN4FLGBC.js.map → cli-skill-templates-D6S2OQ6X.js.map} +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.js +30118 -3253
  14. package/dist/index.js.map +1 -1
  15. package/dist/metafile-esm.json +1 -1
  16. package/package.json +22 -18
  17. package/templates/README.md +45 -30
  18. package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -10
  19. package/templates/apps/admin/src/routes/+page.svelte.hbs +4 -4
  20. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +17 -7
  21. package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +26 -6
  22. package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +2 -2
  23. package/templates/apps/api/.dockerignore.hbs +10 -0
  24. package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +8 -0
  25. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +6 -1
  26. package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +13 -5
  27. package/templates/apps/api/apps/{{projectName}}-contract/tests/contract.test.ts.hbs +13 -5
  28. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  29. package/templates/apps/api/apps/{{projectName}}-serv/README.md.hbs +6 -2
  30. package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +24 -2
  31. package/templates/apps/api/apps/{{projectName}}-serv/package.json.hbs +4 -4
  32. package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +74 -24
  33. package/templates/apps/api/apps/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  34. package/templates/apps/api/apps/{{projectName}}-serv/src/lib/server/init.ts.hbs +153 -37
  35. package/templates/apps/api/apps/{{projectName}}-serv/src/server/procedures/index.ts.hbs +39 -13
  36. package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +106 -33
  37. package/templates/apps/api/apps/{{projectName}}-serv/tsup.config.ts.hbs +3 -1
  38. package/templates/apps/api/apps/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  39. package/templates/apps/api/e2e/api-serv.spec.ts.hbs +20 -3
  40. package/templates/apps/api/e2e/capture-evidence.ts.hbs +4 -1
  41. package/templates/apps/api/eslint.config.js.hbs +3 -1
  42. package/templates/apps/api/images/serv.Dockerfile.hbs +15 -10
  43. package/templates/apps/api/playwright.config.ts.hbs +23 -19
  44. package/templates/apps/api/pnpm-workspace.yaml.hbs +27 -16
  45. package/templates/apps/fullstack/.dockerignore.hbs +10 -0
  46. package/templates/apps/fullstack/.gitignore.hbs +7 -2
  47. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +14 -12
  48. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +5 -3
  49. package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +10 -2
  50. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +1 -1
  51. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +10 -2
  52. package/templates/apps/fullstack/apps/{{projectName}}-app/src/build-metadata.ts.hbs +0 -1
  53. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +0 -1
  54. package/templates/apps/fullstack/apps/{{projectName}}-app/svelte.config.js.hbs +4 -1
  55. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +16 -4
  56. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +10 -8
  57. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +3 -3
  58. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +8 -2
  59. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/build-metadata.ts.hbs +0 -1
  60. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +0 -2
  61. package/templates/apps/fullstack/apps/{{projectName}}-desktop/svelte.config.js.hbs +4 -1
  62. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +16 -4
  63. package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +2 -0
  64. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +9 -9
  65. package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +10 -2
  66. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +1 -0
  67. package/templates/apps/fullstack/apps/{{projectName}}-web/src/build-metadata.ts.hbs +0 -1
  68. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +0 -2
  69. package/templates/apps/fullstack/apps/{{projectName}}-web/svelte.config.js.hbs +4 -1
  70. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +19 -5
  71. package/templates/apps/fullstack/docs/security-configuration.md.hbs +4 -1
  72. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +140 -28
  73. package/templates/apps/fullstack/e2e/session-close.spec.ts.hbs +19 -5
  74. package/templates/apps/fullstack/eslint.config.js.hbs +3 -1
  75. package/templates/apps/fullstack/images/serv.Dockerfile.hbs +19 -11
  76. package/templates/apps/fullstack/images/web.Dockerfile.hbs +17 -8
  77. package/templates/apps/fullstack/package.json.hbs +2 -1
  78. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +7 -5
  79. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +114 -23
  80. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +57 -11
  81. package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +3 -1
  82. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +17 -7
  83. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +13 -7
  84. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +17 -7
  85. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +10 -4
  86. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +65 -30
  87. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/build-metadata.ts.hbs +0 -1
  88. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +1 -2
  89. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +69 -21
  90. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/business-settings.ts.hbs +21 -5
  91. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +63 -11
  92. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -1
  93. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -1
  94. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +147 -33
  95. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/security-roles.ts.hbs +43 -12
  96. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +146 -30
  97. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +33 -10
  98. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/security.test.ts.hbs +142 -26
  99. package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +6 -2
  100. package/templates/apps/fullstack/packages/{{projectName}}-serv/vitest.config.ts.hbs +3 -1
  101. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +48 -66
  102. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +1 -0
  103. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +1 -0
  104. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +1 -0
  105. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +9 -2
  106. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/feedback-client.ts.hbs +0 -3
  107. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/sensitive-actions.svelte.ts.hbs +15 -3
  108. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +33 -7
  109. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/FeedbackEntry.svelte.hbs +16 -2
  110. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +104 -13
  111. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/feedback.svelte.hbs +37 -7
  112. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/roles.svelte.hbs +9 -3
  113. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +25 -3
  114. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +133 -19
  115. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/security/role-label.ts.hbs +14 -2
  116. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +66 -34
  117. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +132 -30
  118. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +8 -2
  119. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +23 -5
  120. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +143 -21
  121. package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -2
  122. package/templates/apps/fullstack/playwright.config.ts.hbs +25 -3
  123. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +30 -14
  124. package/templates/apps/h5/src/routes/+layout.svelte.hbs +74 -30
  125. package/templates/apps/h5/src/routes/+page.svelte.hbs +2 -2
  126. package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +2 -2
  127. package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +2 -2
  128. package/templates/apps/mobile-app/capacitor.config.ts.hbs +5 -2
  129. package/templates/apps/mobile-app/messages/en-US.json +2 -2
  130. package/templates/apps/mobile-app/messages/ja-JP.json +2 -2
  131. package/templates/apps/mobile-app/messages/zh-CN.json +2 -2
  132. package/templates/apps/mobile-app/src/App.svelte.hbs +17 -5
  133. package/templates/apps/mobile-app/src/lib/api.ts.hbs +20 -6
  134. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +8 -1
  135. package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +6 -2
  136. package/templates/apps/mobile-app/src/lib/pages/profile.svelte.hbs +13 -1
  137. package/templates/apps/mobile-app/src/lib/pages/tasks.svelte.hbs +5 -1
  138. package/templates/apps/website/scripts/static-website.mjs.hbs +19 -6
  139. package/templates/apps/website/src/routes/+layout.svelte.hbs +56 -29
  140. package/templates/apps/website/src/routes/+page.svelte.hbs +4 -4
  141. package/templates/apps/website/src/routes/about/+page.svelte.hbs +2 -2
  142. package/templates/apps/website/src/routes/contact/+page.svelte.hbs +2 -2
  143. package/templates/apps/website/src/routes/services/+page.svelte.hbs +20 -5
  144. package/templates/base/.dockerignore.hbs +10 -0
  145. package/templates/base/.gitignore.hbs +7 -2
  146. package/templates/base/e2e/app.spec.ts.hbs +130 -36
  147. package/templates/base/eslint-rules.js +44 -25
  148. package/templates/base/eslint.config.js.hbs +3 -1
  149. package/templates/base/images/web.Dockerfile.hbs +25 -14
  150. package/templates/base/package.json.hbs +13 -7
  151. package/templates/base/patches/daisyui-5.7.50.patch +8 -0
  152. package/templates/base/patches/deepmerge-4.3.1.patch +51 -0
  153. package/templates/base/playwright.config.ts +3 -1
  154. package/templates/base/pnpm-workspace.yaml.hbs +33 -16
  155. package/templates/base/src/app.d.ts.hbs +1 -1
  156. package/templates/base/src/app.html.hbs +1 -0
  157. package/templates/base/src/hooks.server.ts.hbs +44 -33
  158. package/templates/base/src/lib/build-metadata.ts.hbs +0 -1
  159. package/templates/base/src/lib/server/init.ts.hbs +146 -35
  160. package/templates/base/src/lib/server/kit-config.ts.hbs +7 -4
  161. package/templates/base/src/routes/+layout.server.ts.hbs +2 -2
  162. package/templates/base/svelte.config.js.hbs +10 -100
  163. package/templates/base/tests/scaffold.test.ts.hbs +12 -3
  164. package/templates/base/tsconfig.json.hbs +7 -4
  165. package/templates/base/vite.config.ts.hbs +108 -14
  166. package/templates/base/vitest.config.ts.hbs +6 -2
  167. package/templates/features/ai/routes/api/chat/+server.ts +3 -1
  168. package/templates/features/iam/routes-admin/(auth)/auth/forgot-password/+page.svelte.hbs +3 -3
  169. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +3 -3
  170. package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +3 -3
  171. package/templates/features/iam/routes-admin/(auth)/auth/reset-password/+page.svelte.hbs +3 -3
  172. package/templates/features/iam/routes-h5/auth/forgot-password/+page.svelte.hbs +3 -3
  173. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +3 -3
  174. package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +3 -3
  175. package/templates/features/iam/routes-h5/auth/reset-password/+page.svelte.hbs +3 -3
  176. package/templates/features/iam/routes-website/auth/forgot-password/+page.svelte.hbs +3 -3
  177. package/templates/features/iam/routes-website/auth/login/+page.svelte.hbs +3 -3
  178. package/templates/features/iam/routes-website/auth/register/+page.svelte.hbs +3 -3
  179. package/templates/features/iam/routes-website/auth/reset-password/+page.svelte.hbs +3 -3
  180. package/templates/features/payment/routes/api/v1/payment/create/+server.ts +14 -3
  181. package/templates/shared/browser-check/startup.js +115 -0
  182. package/templates/shared/browser-check/update.css +37 -0
  183. package/templates/shared/browser-check/update.html +19 -0
  184. package/templates/shared/browser-startup.spec.ts +23 -0
  185. package/templates/shared/build-metadata.ts +18 -4
  186. package/templates/shared/e2e-env.ts +22 -6
  187. package/templates/shared/native-name.d.mts +5 -0
  188. package/templates/shared/native-name.mjs +94 -0
  189. package/templates/shared/public-base-path.mjs +17 -4
  190. package/templates/shared/run-e2e.mjs +46 -12
  191. package/templates/shared/run-node.mjs +56 -0
  192. package/templates/shared/run-tauri.mjs +74 -0
  193. package/templates/shared/vite-runtime-config.ts +9 -3
  194. package/templates/skills/AGENTS.md +10 -73
  195. package/templates/skills/bridges/admin/AGENTS.md +9 -65
  196. package/templates/skills/bridges/api/AGENTS.md +8 -59
  197. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +9 -54
  198. package/templates/skills/bridges/h5/AGENTS.md +8 -63
  199. package/templates/skills/bridges/mobile-app/AGENTS.md +8 -62
  200. package/templates/skills/bridges/website/AGENTS.md +8 -63
  201. package/templates/skills/hai-ai/SKILL.md +6 -2
  202. package/templates/skills/hai-ai/acp.md +153 -0
  203. package/templates/skills/hai-ai/guide.md +27 -12
  204. package/templates/skills/hai-ai/reference.md +24 -15
  205. package/templates/skills/hai-ai/resources.md +69 -0
  206. package/templates/skills/hai-api-client/SKILL.md +1 -1
  207. package/templates/skills/hai-api-client/reference.md +3 -1
  208. package/templates/skills/hai-app-create/collaboration.md +7 -0
  209. package/templates/skills/hai-app-create/development.md +19 -10
  210. package/templates/skills/hai-app-create/project.md +33 -0
  211. package/templates/skills/hai-app-create/reference.md +10 -10
  212. package/templates/skills/hai-app-tests/reference.md +1 -1
  213. package/templates/skills/hai-app-tests/validation.md +1 -0
  214. package/templates/skills/hai-build/SKILL.md +19 -9
  215. package/templates/skills/hai-build/paths.md +4 -2
  216. package/templates/skills/hai-build/reference.md +26 -8
  217. package/templates/skills/hai-cache/reference.md +6 -2
  218. package/templates/skills/hai-capacitor/SKILL.md +1 -0
  219. package/templates/skills/hai-capacitor/reference.md +24 -0
  220. package/templates/skills/hai-ci/SKILL.md +3 -0
  221. package/templates/skills/hai-core/SKILL.md +9 -4
  222. package/templates/skills/hai-core/reference.md +40 -10
  223. package/templates/skills/hai-core/request-access.md +2 -2
  224. package/templates/skills/hai-crypto/SKILL.md +2 -2
  225. package/templates/skills/hai-crypto/reference.md +7 -3
  226. package/templates/skills/hai-deploy/SKILL.md +41 -5
  227. package/templates/skills/hai-deploy/middleware.md +26 -14
  228. package/templates/skills/hai-framework-sync/SKILL.md +13 -9
  229. package/templates/skills/hai-framework-sync/sync.md +2 -0
  230. package/templates/skills/hai-iam/reference.md +2 -2
  231. package/templates/skills/hai-kit/SKILL.md +1 -1
  232. package/templates/skills/hai-kit/reference.md +19 -5
  233. package/templates/skills/hai-payment/reference.md +41 -17
  234. package/templates/skills/hai-reach/SKILL.md +3 -1
  235. package/templates/skills/hai-reach/reference.md +29 -9
  236. package/templates/skills/hai-reldb/SKILL.md +1 -1
  237. package/templates/skills/hai-reldb/reference.md +9 -2
  238. package/templates/skills/hai-scheduler/reference.md +1 -1
  239. package/templates/skills/hai-serv/SKILL.md +2 -1
  240. package/templates/skills/hai-serv/reference.md +26 -15
  241. package/templates/skills/hai-storage/reference.md +23 -6
  242. package/templates/skills/hai-ui/SKILL.md +11 -6
  243. package/templates/skills/hai-ui/charts.md +51 -0
  244. package/templates/skills/hai-ui/guide.md +1 -1
  245. package/templates/skills/hai-ui/reference.md +58 -38
  246. package/templates/skills/hai-vecdb/reference.md +8 -3
  247. package/dist/chunk-AKV2CQZV.js.map +0 -1
  248. package/dist/chunk-EF7WOFWZ.js.map +0 -1
  249. package/dist/chunk-MUHRBJSW.js +0 -29272
  250. package/dist/chunk-MUHRBJSW.js.map +0 -1
  251. package/dist/cli-config-templates-HHFHNXBN.js +0 -5
  252. package/dist/src-TEBHR5I5.js +0 -5
  253. package/dist/src-TEBHR5I5.js.map +0 -1
  254. package/templates/apps/fullstack/playwright.session-close.config.ts.hbs +0 -18
@@ -35,7 +35,13 @@ async function capture(page: Page, name: string, fallback: string) {
35
35
  await mkdir(evidence, { recursive: true })
36
36
  await expect(page.getByTestId('guide-loading')).toHaveCount(0)
37
37
  await page.evaluate(() => document.fonts.ready)
38
- await page.screenshot({ path: evidence ? path.join(evidence, name) : fallback, fullPage: true, animations: 'disabled' })
38
+ await page.screenshot({
39
+ path: evidence
40
+ ? path.join(evidence, name)
41
+ : fallback,
42
+ fullPage: true,
43
+ animations: 'disabled',
44
+ })
39
45
  }
40
46
 
41
47
  async function dismissToasts(page: Page) {
@@ -62,13 +68,18 @@ async function login(page: Page, username: string, password: string, keepGuide =
62
68
  async function userEditor(page: Page, username: string) {
63
69
  await page.getByTestId('users-search').locator('input').fill(username)
64
70
  await page.getByTestId('users-page').getByRole('button', { name: /查询|Search/ }).click()
65
- const row = page.getByRole('row').filter({ has: page.getByText(`@${username}`, { exact: true }) })
71
+ const row = page.getByRole('row').filter({
72
+ has: page.getByText(`@${username}`, { exact: true }),
73
+ })
66
74
  await expect(row).toBeVisible()
67
75
  await row.getByRole('button', { name: /编辑|Edit/, exact: true }).click()
68
76
  return row
69
77
  }
70
78
 
71
- for (const scenario of [{ offset: -600_000, width: 1440 }, { offset: 600_000, width: 390 }]) {
79
+ for (const scenario of [
80
+ { offset: -600_000, width: 1440 },
81
+ { offset: 600_000, width: 390 },
82
+ ]) {
72
83
  test(`clock offset ${scenario.offset} displays synchronization advice and recovers after correction`, async ({ page }, testInfo) => {
73
84
  await page.setViewportSize({ width: scenario.width, height: 900 })
74
85
  await page.addInitScript((offset) => {
@@ -78,6 +89,8 @@ for (const scenario of [{ offset: -600_000, width: 1440 }, { offset: 600_000, wi
78
89
  await page.goto('./')
79
90
  const advice = page.getByRole('alert').filter({ hasText: /同步|synchron|時刻/i })
80
91
  await expect(advice).toBeVisible()
92
+ await expect(advice).toContainText(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC/)
93
+ await expect(advice).toContainText(/请开启设备的自动校时或手工修改时间|set your device time manually|手動で時刻を設定/i)
81
94
  await expect(advice).toContainText(/刷新|refresh|再読み込み/i)
82
95
  await capture(page, `clock-skew-${scenario.width}.png`, testInfo.outputPath(`clock-skew-${scenario.width}.png`))
83
96
  // 校时后由用户明确刷新;不能自动重放之前失败的业务请求。
@@ -97,10 +110,11 @@ test('service health endpoint is ready', async ({ request }) => {
97
110
  })
98
111
 
99
112
  test('service rejects plaintext business requests', async ({ request }) => {
100
- const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, { data: { message: 'hello e2e' } })
113
+ const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, {
114
+ data: { message: 'hello e2e' },
115
+ })
101
116
  expect(response.status()).toBe(400)
102
117
  })
103
-
104
118
  {{#if fullstack.e2eFrontend}}
105
119
  test('hash routing and responsive workspace preserve usable content width', async ({ page }, testInfo) => {
106
120
  await page.goto('./#/roles?source=direct')
@@ -287,7 +301,11 @@ const additionalFrontends = [
287
301
 
288
302
  for (const frontend of additionalFrontends) {
289
303
  test(`${frontend.target} preview supports login, tour, help and profile navigation`, async ({ browser }, testInfo) => {
290
- const page = await browser.newPage({ viewport: frontend.target === 'app' ? { width: 390, height: 844 } : { width: 1440, height: 960 } })
304
+ const page = await browser.newPage({
305
+ viewport: frontend.target === 'app'
306
+ ? { width: 390, height: 844 }
307
+ : { width: 1440, height: 960 },
308
+ })
291
309
  const pageErrors: string[] = []
292
310
  page.on('pageerror', error => pageErrors.push(error.message))
293
311
  try {
@@ -327,11 +345,17 @@ test('shared authentication supports public registration with configuration', as
327
345
  await mkdir(evidence, { recursive: true })
328
346
  await page.goto('./')
329
347
  await expect(page.getByTestId('login-form')).toBeVisible()
330
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-login.png'), fullPage: true })
348
+ await page.screenshot({
349
+ path: path.join(evidence, '{{projectName}}-login.png'),
350
+ fullPage: true,
351
+ })
331
352
  await page.getByRole('button', { name: /立即注册|Register now/i }).click()
332
353
  const registerForm = page.locator('.register-form')
333
354
  await expect(registerForm.locator('#register-username')).toBeVisible()
334
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-register.png'), fullPage: true })
355
+ await page.screenshot({
356
+ path: path.join(evidence, '{{projectName}}-register.png'),
357
+ fullPage: true,
358
+ })
335
359
  const username = `signup${randomUUID().replaceAll('-', '').slice(0, 12)}`
336
360
  await registerForm.locator('#register-username').fill(username)
337
361
  await registerForm.locator('#register-email').fill(`${username}@example.com`)
@@ -345,7 +369,10 @@ test('shared authentication supports public registration with configuration', as
345
369
  const email = `${username}@example.com`
346
370
  await page.getByTestId('logout').click()
347
371
  await page.getByRole('button', { name: /邮件验证码|Email code/i, exact: true }).click()
348
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-email.png'), fullPage: true })
372
+ await page.screenshot({
373
+ path: path.join(evidence, '{{projectName}}-email.png'),
374
+ fullPage: true,
375
+ })
349
376
  await page.locator('#login-username').fill(email)
350
377
  await page.getByRole('button', { name: /发送验证码|Send code/i, exact: true }).click()
351
378
  await expect(page.getByRole('button', { name: /秒后重发|Resend in/ })).toBeDisabled()
@@ -361,14 +388,20 @@ test('shared authentication supports public registration with configuration', as
361
388
  await expect(page.getByTestId('settings-page')).toBeVisible()
362
389
  await page.getByTestId('logout').click()
363
390
  await page.getByRole('button', { name: /忘记密码|Forgot password/i }).click()
364
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-forgot.png'), fullPage: true })
391
+ await page.screenshot({
392
+ path: path.join(evidence, '{{projectName}}-forgot.png'),
393
+ fullPage: true,
394
+ })
365
395
  await page.locator('input[type="email"]').fill(email)
366
396
  await page.locator('button[type="submit"]').click()
367
397
  await expect(page.locator('.hai-toast-item[data-variant="success"]')).toBeVisible()
368
398
  const reset = await received('password_reset')
369
399
  expect(Boolean(reset?.token)).toBe(true)
370
400
  await page.getByRole('button', { name: /已有重置令牌|I have a reset token/i }).click()
371
- await page.screenshot({ path: path.join(evidence, '{{projectName}}-reset.png'), fullPage: true })
401
+ await page.screenshot({
402
+ path: path.join(evidence, '{{projectName}}-reset.png'),
403
+ fullPage: true,
404
+ })
372
405
  await page.goto(`./?token=${encodeURIComponent(reset?.token ?? '')}`)
373
406
  await expect(page.locator('input[type="password"]').nth(0)).toBeVisible()
374
407
  await page.locator('input[type="password"]').nth(0).fill('FullstackReset456')
@@ -399,7 +432,12 @@ test('frontend displays configured core metadata and restores the administrator
399
432
  const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
400
433
  if (evidence)
401
434
  await mkdir(evidence, { recursive: true })
402
- await page.screenshot({ path: evidence ? path.join(evidence, '{{projectName}}-users.png') : testInfo.outputPath('users.png'), fullPage: true })
435
+ await page.screenshot({
436
+ path: evidence
437
+ ? path.join(evidence, '{{projectName}}-users.png')
438
+ : testInfo.outputPath('users.png'),
439
+ fullPage: true,
440
+ })
403
441
  await page.getByTestId('logout').click()
404
442
  await expect(page.getByTestId('login-form')).toBeVisible()
405
443
  })
@@ -522,9 +560,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
522
560
  await page.setViewportSize({ width, height: 900 })
523
561
  if (width < 1024)
524
562
  await expect.poll(() => page.locator('aside').evaluate(sidebar => sidebar.getBoundingClientRect().right)).toBeLessThanOrEqual(1)
525
- const controls = await page.getByTestId('audit-advanced').evaluate(panel => [...panel.querySelectorAll('input')].map((element) => {
563
+ const controls = await page.getByTestId('audit-advanced').evaluate(panel => [
564
+ ...panel.querySelectorAll('input'),
565
+ ].map((element) => {
526
566
  // 复合控件的点击框包含边框和留白,内部文字输入区不等于完整控件高度。
527
- const control = element.closest('.fieldset') ? element.parentElement! : element
567
+ const control = element.closest('.fieldset')
568
+ ? element.parentElement!
569
+ : element
528
570
  const rect = control.getBoundingClientRect()
529
571
  return { left: rect.left, right: rect.right, height: rect.height }
530
572
  }))
@@ -540,7 +582,13 @@ test('audit filters collapse without losing values and fit compact workspaces',
540
582
  if (evidence)
541
583
  await mkdir(evidence, { recursive: true })
542
584
  const file = `{{projectName}}-audit-${width}.png`
543
- await page.screenshot({ path: evidence ? path.join(evidence, file) : testInfo.outputPath(file), fullPage: true, animations: 'disabled' })
585
+ await page.screenshot({
586
+ path: evidence
587
+ ? path.join(evidence, file)
588
+ : testInfo.outputPath(file),
589
+ fullPage: true,
590
+ animations: 'disabled',
591
+ })
544
592
  }
545
593
  }
546
594
  const reset = page.waitForResponse(response => new URL(response.url()).pathname.endsWith('/admin/audit/list'))
@@ -561,7 +609,11 @@ test('audit filters collapse without losing values and fit compact workspaces',
561
609
  await toggle.click()
562
610
  await page.getByTestId('audit-page').getByRole('button', { name: /详情|Details/ }).first().click()
563
611
  await expect(page.getByRole('dialog')).toBeVisible()
564
- await page.screenshot({ path: testInfo.outputPath('audit-short-table-details.png'), fullPage: true, animations: 'disabled' })
612
+ await page.screenshot({
613
+ path: testInfo.outputPath('audit-short-table-details.png'),
614
+ fullPage: true,
615
+ animations: 'disabled',
616
+ })
565
617
  })
566
618
 
567
619
  test('administrator manages every duty through one lifecycle and the shared password reset form', async ({ page, browser }, testInfo) => {
@@ -647,7 +699,7 @@ test('administrator manages every duty through one lifecycle and the shared pass
647
699
  await expect(row).toHaveCount(0)
648
700
  })
649
701
 
650
- test('administrator and user complete account, avatar, preference, password and audit workflows', async ({ page, browser }) => {
702
+ test('administrator and user complete account, avatar, preference, password and audit workflows', async ({ page, browser }, testInfo) => {
651
703
  test.setTimeout(120_000)
652
704
  const username = `member-${randomUUID().slice(0, 8)}`
653
705
  const password = 'User-test-Pass123!'
@@ -716,8 +768,16 @@ test('administrator and user complete account, avatar, preference, password and
716
768
  await userPage.getByRole('option', { name: 'English', exact: true }).click()
717
769
  await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
718
770
  await expect(userPage.locator('html')).toHaveAttribute('lang', 'en-US')
719
- const themes = [{{#each fullstack.theme.themes}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]
720
- const selectedTheme = themes.includes('formal') ? 'formal' : themes.includes('high-contrast') ? 'high-contrast' : themes.find(theme => theme !== '{{fullstack.theme.defaultTheme}}') ?? themes[0]
771
+ const themes = [
772
+ {{#each fullstack.theme.themes}}
773
+ '{{this}}',
774
+ {{/each}}
775
+ ]
776
+ const selectedTheme = themes.includes('formal')
777
+ ? 'formal'
778
+ : themes.includes('high-contrast')
779
+ ? 'high-contrast'
780
+ : themes.find(theme => theme !== '{{fullstack.theme.defaultTheme}}') ?? themes[0]
721
781
  await userPage.getByTestId('profile-theme').click()
722
782
  await userPage.getByRole('option').nth(themes.indexOf(selectedTheme)).click()
723
783
  await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
@@ -727,6 +787,20 @@ test('administrator and user complete account, avatar, preference, password and
727
787
  await expect(userPage.getByTestId('settings-page')).toBeVisible()
728
788
  await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
729
789
  await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
790
+ if (themes.includes('eye-care')) {
791
+ await userPage.getByTestId('profile-theme').click()
792
+ await userPage.getByRole('option', { name: 'Eye care', exact: true }).click()
793
+ await expect(userPage.locator('html')).toHaveAttribute('data-theme', 'eye-care')
794
+ await userPage.reload()
795
+ await expect(userPage.getByTestId('settings-page')).toBeVisible()
796
+ await expect(userPage.locator('html')).toHaveAttribute('data-theme', 'eye-care')
797
+ await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).colorScheme)).toBe('light')
798
+ await expect.poll(() => userPage.evaluate(() => getComputedStyle(document.documentElement).getPropertyValue('--color-base-100').trim())).toBe('#e8f0df')
799
+ await userPage.screenshot({
800
+ path: testInfo.outputPath('fullstack-eye-care.png'),
801
+ animations: 'disabled',
802
+ })
803
+ }
730
804
  await expect(userPage.getByTestId('profile-form').locator('input[name="displayName"]')).toHaveValue('Updated member')
731
805
  await expect(userPage.getByTestId('profile-form').locator('input[name="email"]')).toHaveValue('')
732
806
  await expect.poll(() => userPage.getByTestId('profile-form').locator('img').evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true)
@@ -805,6 +879,8 @@ test('administrator and user complete account, avatar, preference, password and
805
879
  {{#if fullstack.e2eFrontend}}
806
880
 
807
881
  test('feedback submits an empty description and business operator can process and reopen it', async ({ page }, testInfo) => {
882
+ // 附件脱敏、桌面/手机截图和导出校验共用完整流程预算,目录代理也执行相同断言。
883
+ test.setTimeout(120_000)
808
884
  await page.goto('./')
809
885
  await login(page, 'business_operator', businessOperatorPassword)
810
886
  const feedbackImage = Buffer.from(await page.evaluate(() => {
@@ -853,7 +929,11 @@ test('feedback submits an empty description and business operator can process an
853
929
  await dialog.locator('input[type=file]').setInputFiles([
854
930
  { name: 'attached-preview.png', mimeType: 'image/png', buffer: feedbackImage },
855
931
  { name: 'attached-large-log.txt', mimeType: 'text/plain', buffer: feedbackLog },
856
- { name: 'discarded.txt', mimeType: 'text/plain', buffer: Buffer.from('discarded local material') },
932
+ {
933
+ name: 'discarded.txt',
934
+ mimeType: 'text/plain',
935
+ buffer: Buffer.from('discarded local material'),
936
+ },
857
937
  ])
858
938
  const uploaded = dialog.locator('img[alt="attached-preview.png"]')
859
939
  await expect(uploaded).toBeVisible()
@@ -861,7 +941,10 @@ test('feedback submits an empty description and business operator can process an
861
941
  await expectDialogViewport()
862
942
  await expectUncovered(uploaded)
863
943
  await capture(page, 'feedback-image-before-mask.png', testInfo.outputPath('feedback-image-before-mask.png'))
864
- await uploaded.screenshot({ path: testInfo.outputPath('feedback-image-before-mask-detail.png'), animations: 'disabled' })
944
+ await uploaded.screenshot({
945
+ path: testInfo.outputPath('feedback-image-before-mask-detail.png'),
946
+ animations: 'disabled',
947
+ })
865
948
  const originalPreview = await uploaded.getAttribute('src')
866
949
  await dialog.getByRole('button', { name: /遮挡敏感区域|Mask sensitive area|機密情報を隠す/ }).last().click()
867
950
  await dialog.getByRole('button', { name: /应用标注|Apply annotation|注釈を適用/ }).last().click()
@@ -886,7 +969,15 @@ test('feedback submits an empty description and business operator can process an
886
969
  await expectDialogViewport()
887
970
  await expectUncovered(uploaded)
888
971
  await capture(page, 'feedback-preview.png', testInfo.outputPath('feedback-preview.png'))
889
- await uploaded.screenshot({ path: testInfo.outputPath('feedback-image-after-mask-detail.png'), animations: 'disabled' })
972
+ await uploaded.screenshot({
973
+ path: testInfo.outputPath('feedback-image-after-mask-detail.png'),
974
+ animations: 'disabled',
975
+ })
976
+ // 回执只表示记录已受理;等这份大附件真正上传成功,再开始后台处理操作。
977
+ const largeUpload = page.waitForResponse(response => response.request().method() === 'POST'
978
+ && /\/feedback\/reports\/[^/]+\/attachments\/[^/]+$/.test(new URL(response.url()).pathname)
979
+ && (response.request().postDataBuffer()?.byteLength ?? 0) > 2 * 1024 * 1024, { timeout: 60_000 })
980
+ .then(response => ({ response }), (error: unknown) => ({ error }))
890
981
  await dialog.getByRole('button', { name: /提交反馈|Submit Feedback|フィードバックを送信/ }).click()
891
982
  await expect(dialog.getByTestId('feedback-receipt')).toContainText(/FB-/)
892
983
  const number = (await dialog.getByTestId('feedback-receipt').textContent())?.match(/FB-[A-Za-z0-9-]+/)?.[0]
@@ -895,13 +986,20 @@ test('feedback submits an empty description and business operator can process an
895
986
  throw new Error('Feedback receipt number is missing')
896
987
  await expectDialogViewport()
897
988
  await capture(page, 'feedback-accepted.png', testInfo.outputPath('feedback-accepted.png'))
989
+ const completedUpload = await largeUpload
990
+ if ('error' in completedUpload)
991
+ throw completedUpload.error
992
+ expect(completedUpload.response.status()).toBe(200)
898
993
  await dialog.getByRole('button', { name: /关闭|Close|閉じる/ }).filter({ hasText: /关闭|Close|閉じる/ }).click()
899
994
  await page.getByTestId('nav-feedback').click()
900
995
  await expect(page.getByTestId('feedback-page')).toBeVisible()
901
996
  const row = page.getByRole('row').filter({ hasText: number })
902
997
  await expect(row).toBeVisible()
903
- await row.getByRole('button', { name: /标记已处理|Mark handled|処理済み/ }).click()
904
- await expect(row).toContainText(/已处理|Handled|処理済み/)
998
+ const handled = page.waitForResponse(response => response.request().method() === 'PATCH' && new URL(response.url()).pathname.endsWith('/status'))
999
+ await row.getByRole('button', { name: /标记已处理|Mark(?: as)? handled|処理済み/ }).click()
1000
+ expect((await handled).status()).toBe(200)
1001
+ // 仅核对状态徽标;“标记已处理”按钮本身也包含“已处理”,不能据此放行。
1002
+ await expect(row.getByText(/^(已处理|Handled|処理済み)$/)).toBeVisible()
905
1003
  await row.getByRole('button', { name: /撤销处理|Reopen|処理を取り消す/ }).scrollIntoViewIfNeeded()
906
1004
  await capture(page, 'feedback-handled.png', testInfo.outputPath('feedback-handled.png'))
907
1005
  await row.getByRole('button', { name: /撤销处理|Reopen|処理を取り消す/ }).click()
@@ -945,7 +1043,12 @@ test('feedback submits an empty description and business operator can process an
945
1043
  const serverManifest: { version: string } = JSON.parse(await readFile(path.resolve('packages/{{projectName}}-serv/package.json'), 'utf8'))
946
1044
  let expectedCommit = 'unknown'
947
1045
  try {
948
- execFileSync('git', ['ls-files', '--error-unmatch', '--', 'package.json'], { stdio: 'pipe', windowsHide: true })
1046
+ execFileSync('git', [
1047
+ 'ls-files',
1048
+ '--error-unmatch',
1049
+ '--',
1050
+ 'package.json',
1051
+ ], { stdio: 'pipe', windowsHide: true })
949
1052
  expectedCommit = execFileSync('git', ['rev-parse', 'HEAD'], { encoding: 'utf8', stdio: 'pipe', windowsHide: true }).trim()
950
1053
  }
951
1054
  catch { /* 无 Git 的源码归档必须明确保留 unknown,不冒用父工程提交。 */ }
@@ -954,7 +1057,10 @@ test('feedback submits an empty description and business operator can process an
954
1057
  expect(expectedCommit).toMatch(/^(?:[a-f\d]{40}|[a-f\d]{64})$/i)
955
1058
  expect(browserManifest.version).not.toBe(serverManifest.version)
956
1059
  }
957
- for (const [metadata, version] of [[browserMetadata, browserManifest.version], [serverMetadata, serverManifest.version]] as const) {
1060
+ for (const [metadata, version] of [
1061
+ [browserMetadata, browserManifest.version],
1062
+ [serverMetadata, serverManifest.version],
1063
+ ] as const) {
958
1064
  expect(Object.keys(metadata).sort()).toEqual(['appVersion', 'buildId', 'commit'])
959
1065
  expect(metadata.appVersion).toBe(version)
960
1066
  expect(metadata.buildId).toMatch(/^[\w.+-]{1,160}$/)
@@ -966,7 +1072,10 @@ test('feedback submits an empty description and business operator can process an
966
1072
  expect(browserEnvironment?.data).toMatchObject(browserMetadata)
967
1073
  expect(serverEnvironment?.data.service).toMatchObject(serverMetadata)
968
1074
  expect(bundle.report).toMatchObject(browserMetadata)
969
- await testInfo.attach('feedback-build-metadata', { body: JSON.stringify({ browser: browserMetadata, server: serverMetadata }), contentType: 'application/json' })
1075
+ await testInfo.attach('feedback-build-metadata', {
1076
+ body: JSON.stringify({ browser: browserMetadata, server: serverMetadata }),
1077
+ contentType: 'application/json',
1078
+ })
970
1079
  expect(bundle.report.number).toBe(number)
971
1080
  expect(bundle.manifest.reportId).toBe(bundle.report.id)
972
1081
  expect(bundle.manifest.evidenceRevision).toBe(bundle.report.evidenceRevision)
@@ -982,7 +1091,10 @@ test('feedback submits an empty description and business operator can process an
982
1091
  throw new Error('The exported large log attachment is missing')
983
1092
  expect(bundle.files[largeLog.path]).toEqual(new Uint8Array(feedbackLog))
984
1093
  expect(bundle.manifest.files.some(file => file.name === 'discarded.txt')).toBe(false)
985
- await testInfo.attach('feedback-export-masked', { body: Buffer.from(bundle.files[masked.path]!), contentType: 'image/png' })
1094
+ await testInfo.attach('feedback-export-masked', {
1095
+ body: Buffer.from(bundle.files[masked.path]!),
1096
+ contentType: 'image/png',
1097
+ })
986
1098
  })
987
1099
  {{/if}}
988
1100
  {{/if}}
@@ -20,8 +20,13 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
20
20
  // 每例使用独立初始账号,真实完成服务端限制下的首次改密后才能建立普通会话。
21
21
  await expect(page.locator('#old-password')).toBeVisible()
22
22
  await expect(page.getByTestId('logout')).toHaveCount(0)
23
- await page.screenshot({ path: test.info().outputPath('initial-password-renewal.png'), fullPage: true })
24
- const form = page.locator('form').filter({ has: page.locator('#old-password') })
23
+ await page.screenshot({
24
+ path: test.info().outputPath('initial-password-renewal.png'),
25
+ fullPage: true,
26
+ })
27
+ const form = page.locator('form').filter({
28
+ has: page.locator('#old-password'),
29
+ })
25
30
  const error = page.locator('.hai-toast-item[data-variant=error]')
26
31
  await dismissToasts(page)
27
32
  // 同密码仍由服务端拒绝,显示安全原因与关联编号;本地确认不一致则直接提示字段。
@@ -34,7 +39,10 @@ async function login(page: Page, username = 'admin', initialPassword = process.e
34
39
  await error.getByRole('button', { name: /^(查看排查信息|View diagnostic details|調査情報を表示)$/ }).click()
35
40
  await expect(error.locator('code')).toHaveText(/^[\w.:-]{1,128}$/)
36
41
  await expect(page.getByTestId('logout')).toHaveCount(0)
37
- await page.screenshot({ path: test.info().outputPath('initial-password-same.png'), fullPage: true })
42
+ await page.screenshot({
43
+ path: test.info().outputPath('initial-password-same.png'),
44
+ fullPage: true,
45
+ })
38
46
  await expect(form.locator('.alert-error')).toHaveCount(0)
39
47
  await dismissToasts(page)
40
48
  const password = `${initialPassword}-Renewed!`
@@ -100,13 +108,19 @@ test('server idle expiry opens normal login in place and resumes one rejected sa
100
108
  const dialog = page.getByTestId('session-recovery-dialog')
101
109
  await expect(dialog).toBeVisible()
102
110
  await expect(page.getByTestId('business-notice-input').locator('textarea')).toHaveValue(draft)
103
- await page.screenshot({ path: test.info().outputPath('idle-session-recovery.png'), fullPage: true })
111
+ await page.screenshot({
112
+ path: test.info().outputPath('idle-session-recovery.png'),
113
+ fullPage: true,
114
+ })
104
115
  const form = dialog.locator('form.login-form')
105
116
  await form.locator('input[type=password]').fill('Incorrect-test-password!')
106
117
  await form.locator('button[type=submit]').click()
107
118
  await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeVisible()
108
119
  await expect(page.locator('.hai-toast-item[data-variant=error]')).toBeInViewport({ ratio: 1 })
109
- await page.screenshot({ path: test.info().outputPath('idle-session-recovery-error-toast.png'), fullPage: true })
120
+ await page.screenshot({
121
+ path: test.info().outputPath('idle-session-recovery-error-toast.png'),
122
+ fullPage: true,
123
+ })
110
124
  expect(statuses).toHaveLength(1)
111
125
  await form.locator('input[type=password]').fill(password)
112
126
  await form.locator('button[type=submit]').click()
@@ -3,7 +3,9 @@ import { haiQualityConfig } from './eslint-rules.js'
3
3
 
4
4
  export default antfu({
5
5
  // unused-imports 使用 TS 规则,JS 同样使用 TS parser 以保持 scope 契约一致。
6
- typescript: { files: [GLOB_SRC] },
6
+ typescript: {
7
+ files: [GLOB_SRC],
8
+ },
7
9
  formatters: true,
8
10
  ignores: [
9
11
  '**/build/**',
@@ -1,6 +1,5 @@
1
- FROM node:24.22.1-bookworm-slim AS build
2
- ARG SQLITE_PREBUILD_MIRROR=
3
- ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
1
+ ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
2
+ FROM ${HAI_NODE_IMAGE} AS build
4
3
 
5
4
  ENV PNPM_HOME=/pnpm
6
5
  ENV PATH=$PNPM_HOME:$PATH
@@ -8,20 +7,29 @@ ENV CI=true
8
7
  ENV ONNXRUNTIME_NODE_INSTALL_CUDA=skip
9
8
  WORKDIR /workspace
10
9
 
11
- RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
10
+ ARG HAI_NPM_REGISTRY=https://registry.npmjs.org
11
+ ARG COREPACK_NPM_REGISTRY=$HAI_NPM_REGISTRY
12
+ ARG npm_config_registry=$HAI_NPM_REGISTRY
13
+ ARG pnpm_config_registry=$HAI_NPM_REGISTRY
14
+ RUN node -e "const p=process.env.HTTPS_PROXY||process.env.HTTP_PROXY;if(p){const u=new URL(p);const s=require('node:net').connect({host:u.hostname,port:Number(u.port||(u.protocol==='https:'?443:80))});s.setTimeout(3000);s.on('connect',()=>s.end());const fail=()=>{process.stderr.write('Build proxy is unreachable from the container. Check the host address and proxy listener.\\n');process.exit(1)};s.on('error',fail);s.on('timeout',fail)}" && corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
12
15
  COPY pnpm-lock.yaml pnpm-workspace.yaml ./
13
16
  COPY images/dependencies/ ./
14
- RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
15
- COPY . .
16
- RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
17
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --reporter=append-only --ignore-scripts
18
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --reporter=append-only --offline --frozen-lockfile --ignore-scripts
19
+ COPY tsconfig.base.json ./
20
+ COPY scripts/ ./scripts/
21
+ COPY packages/{{projectName}}-contract/ ./packages/{{projectName}}-contract/
22
+ COPY packages/{{projectName}}-serv/ ./packages/{{projectName}}-serv/
17
23
  ARG HAI_BUILD_ID=
18
24
  ARG HAI_BUILD_COMMIT=unknown
19
25
  RUN HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" pnpm --filter {{fullstack.servPackageName}}... --if-present build
20
26
 
21
- 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
- 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
27
+ # SQLite 13 的原生二进制随 npm 包交付,在构建时验证实际加载。
28
+ RUN if [ -d node_modules/better-sqlite3 ]; then node -e "const Database=require('better-sqlite3');const db=new Database(':memory:');db.prepare('SELECT 1').get();db.close()"; fi
29
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm pm deploy --filter {{fullstack.servPackageName}} --prod /prod/service
30
+ 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
31
 
24
- FROM node:24.22.1-bookworm-slim AS service
32
+ FROM ${HAI_NODE_IMAGE} AS service
25
33
 
26
34
  ENV NODE_ENV=production
27
35
  ENV HOST=0.0.0.0
@@ -34,5 +42,5 @@ RUN mkdir -p /app/data && chown node:node /app/data
34
42
  USER node
35
43
  EXPOSE 3000
36
44
  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))"
45
+ CMD node -e "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config', configSchemas: { serv: ServConfigSchema } }); 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
46
  CMD ["node", "dist/index.js"]
@@ -1,25 +1,33 @@
1
+ ARG HAI_NODE_IMAGE=node:24.22.1-bookworm-slim
2
+ ARG HAI_NGINX_IMAGE=nginxinc/nginx-unprivileged:1.29.8-alpine
1
3
  {{#if fullstack.frontends.web}}
2
- FROM node:24.22.1-bookworm-slim AS build
3
- ARG SQLITE_PREBUILD_MIRROR=
4
- ENV npm_config_better_sqlite3_binary_host_mirror=$SQLITE_PREBUILD_MIRROR
4
+ FROM ${HAI_NODE_IMAGE} AS build
5
5
 
6
6
  ENV PNPM_HOME=/pnpm
7
7
  ENV PATH=$PNPM_HOME:$PATH
8
8
  ENV CI=true
9
9
  WORKDIR /workspace
10
10
 
11
- RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
11
+ ARG HAI_NPM_REGISTRY=https://registry.npmjs.org
12
+ ARG COREPACK_NPM_REGISTRY=$HAI_NPM_REGISTRY
13
+ ARG npm_config_registry=$HAI_NPM_REGISTRY
14
+ ARG pnpm_config_registry=$HAI_NPM_REGISTRY
15
+ RUN node -e "const p=process.env.HTTPS_PROXY||process.env.HTTP_PROXY;if(p){const u=new URL(p);const s=require('node:net').connect({host:u.hostname,port:Number(u.port||(u.protocol==='https:'?443:80))});s.setTimeout(3000);s.on('connect',()=>s.end());const fail=()=>{process.stderr.write('Build proxy is unreachable from the container. Check the host address and proxy listener.\\n');process.exit(1)};s.on('error',fail);s.on('timeout',fail)}" && corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
12
16
  COPY pnpm-lock.yaml pnpm-workspace.yaml ./
13
17
  COPY images/dependencies/ ./
14
- RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --ignore-scripts
15
- COPY . .
16
- RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --offline --frozen-lockfile --ignore-scripts
18
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm fetch --reporter=append-only --ignore-scripts
19
+ RUN --mount=type=cache,id={{projectName}}-pnpm,target=/pnpm/store pnpm install --reporter=append-only --offline --frozen-lockfile --ignore-scripts
20
+ COPY tsconfig.base.json ./
21
+ COPY scripts/ ./scripts/
22
+ COPY packages/{{projectName}}-contract/ ./packages/{{projectName}}-contract/
23
+ COPY packages/{{projectName}}-shared/ ./packages/{{projectName}}-shared/
24
+ COPY apps/{{projectName}}-web/ ./apps/{{projectName}}-web/
17
25
  ARG HAI_BUILD_ID=
18
26
  ARG HAI_BUILD_COMMIT=unknown
19
27
  ARG PUBLIC_BASE_PATH=
20
28
  RUN pnpm --filter {{fullstack.sharedPackageName}} paraglide:compile && HAI_BUILD_ID="$HAI_BUILD_ID" HAI_BUILD_COMMIT="$HAI_BUILD_COMMIT" PUBLIC_BASE_PATH="$PUBLIC_BASE_PATH" pnpm --filter {{projectName}}-web... --if-present build
21
29
 
22
- FROM nginxinc/nginx-unprivileged:1.29.8-alpine AS web
30
+ FROM ${HAI_NGINX_IMAGE} AS web
23
31
 
24
32
  ENV PUBLIC_API_BASE=
25
33
  ENV NGINX_ENVSUBST_FILTER=PUBLIC_API_BASE_BASE64
@@ -30,6 +38,7 @@ COPY images/web/nginx.conf /etc/nginx/conf.d/default.conf
30
38
  COPY --chmod=755 images/web/19-runtime-config.envsh /docker-entrypoint.d/19-runtime-config.envsh
31
39
  COPY images/web/runtime-config.js.template /etc/nginx/templates/runtime-config.js.template
32
40
  COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
41
+ RUN find /usr/share/nginx/html -type f -name '*.map' -delete
33
42
 
34
43
  EXPOSE 8080
35
44
  HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
@@ -26,7 +26,7 @@
26
26
  "lint:fix": "eslint *.js *.ts e2e --fix && pnpm -r --if-present lint:fix",
27
27
  "test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present test",
28
28
  "test:watch": "pnpm -r --parallel --if-present test:watch",
29
- "test:e2e": "pnpm compile && node scripts/run-e2e.mjs --config playwright.config.ts{{#if fullstack.e2eFrontend}} --next --config playwright.session-close.config.ts{{/if}}",
29
+ "test:e2e": "pnpm compile && node scripts/run-e2e.mjs --config playwright.config.ts{{#if fullstack.e2eFrontend}} --next --e2e-group=session-close --config playwright.config.ts{{/if}}",
30
30
  "clean": "pnpm -r --if-present clean && rimraf node_modules .turbo"
31
31
  },
32
32
  "dependencies": {
@@ -35,6 +35,7 @@
35
35
  "{{fullstack.sharedPackageName}}": "workspace:*"
36
36
  },
37
37
  "devDependencies": {
38
+ "@h-ai/core": "{{haiPackageSpecifier}}",
38
39
  "@h-ai/cli": "{{haiPackageSpecifier}}",
39
40
  {{#if features.feedback}}
40
41
  "@h-ai/feedback": "{{haiPackageSpecifier}}",
@@ -14,6 +14,13 @@
14
14
 
15
15
  ### 骨架说明
16
16
 
17
+ | 功能 | 输入示例 | 成功输出与边界 |
18
+ | --- | --- | --- |
19
+ | app.info | 无业务参数 | notice、名称/版本/环境、语言、启动时间及frontends;只返回公开元信息 |
20
+ | app.echo | `{ message: 'hello' }`,1–200字符 | 相同message、echoedAt、requestId;演示回显,不持久化 |
21
+ | admin契约 | 用户、角色、审计、公告或个人设置操作参数 | 受当前身份/权限与资源范围约束的HaiResult;先检查success再读data |
22
+ | 工程聚合 | app/admin及框架模块contract | Serv和所有前端共用类型,契约声明不会自动授予权限 |
23
+
17
24
  - `src/app-schemas.ts`:跨接口、跨层复用的输入/数据 Schema 与类型。
18
25
  - `src/app-contract.ts`:app 领域 contract;每个业务 path 直接写在对应接口定义中。
19
26
  - `src/admin-schemas.ts` / `src/admin-contract.ts`:示例管理员用户、系统/审计/配置/业务操作员及基础成员角色、账号生命周期、邀请、用户列表服务端排序、审计分类/列头排序/统计/备份、业务公告和个人设置、加密 JSON 头像上传 Schema/类型/路由;认证复用框架的 IAM auth contract,通过服务端配置启用注册、密码/LDAP/验证码登录及密码重置。
@@ -23,11 +30,6 @@
23
30
  API 前缀、健康检查与跨端 Header 等传输配置位于 serv 包的 `config/_serv.yml`。
24
31
  `SUPPORTED_LOCALES` 为生成时实际编译的语言列表;`app.info` 中名称、版本、环境、默认语言来自后端 core 配置。更新公开契约时同时更新 serv、shared typed API client、各端 UI 与测试。
25
32
 
26
- ### 开发
27
-
28
- - `pnpm --filter {{fullstack.contractPackageName}} typecheck`
29
- - `pnpm --filter {{fullstack.contractPackageName}} test`
30
-
31
33
  ## 开发测试
32
34
 
33
35
  ```sh