@h-ai/cli 0.1.0-alpha.56 → 0.1.0-alpha.58

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 (208) hide show
  1. package/README.en.md +239 -0
  2. package/README.ja.md +239 -0
  3. package/README.md +51 -1
  4. package/dist/chunk-2H7UOFLK.js +0 -0
  5. package/dist/chunk-F4IBPVZ5.js +355 -0
  6. package/dist/chunk-F4IBPVZ5.js.map +1 -0
  7. package/dist/{chunk-O3ANVICQ.js → chunk-WUSFJFW5.js} +13 -7
  8. package/dist/chunk-WUSFJFW5.js.map +1 -0
  9. package/dist/cli-config-templates-6XA7V6CC.js +5 -0
  10. package/dist/{cli-config-templates-OWBEGBZQ.js.map → cli-config-templates-6XA7V6CC.js.map} +1 -1
  11. package/dist/{cli-skill-templates-ZIWYMS4Y.js → cli-skill-templates-45CK4RQ2.js} +3 -3
  12. package/dist/{cli-skill-templates-ZIWYMS4Y.js.map → cli-skill-templates-45CK4RQ2.js.map} +1 -1
  13. package/dist/index.js +134 -125
  14. package/dist/index.js.map +1 -1
  15. package/dist/{src-TBIET7YU.js → src-DUGLX4YB.js} +7270 -2119
  16. package/dist/src-DUGLX4YB.js.map +1 -0
  17. package/package.json +17 -17
  18. package/templates/README.md +4 -4
  19. package/templates/apps/admin/src/routes/+layout.svelte.hbs +7 -0
  20. package/templates/apps/admin/src/routes/+page.svelte.hbs +5 -4
  21. package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +2 -2
  22. package/templates/apps/api/.gitignore.hbs +13 -0
  23. package/templates/apps/api/.npmrc.hbs +1 -0
  24. package/templates/apps/api/Dockerfile.hbs +6 -5
  25. package/templates/apps/api/apps/{{projectName}}-contract/package.json.hbs +1 -1
  26. package/templates/apps/api/apps/{{projectName}}-contract/src/app-schemas.ts.hbs +2 -0
  27. package/templates/apps/api/apps/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
  28. package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +8 -0
  29. package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -1
  30. package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +7 -8
  31. package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +4 -3
  32. package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +0 -1
  33. package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/app-procedures.ts.hbs +4 -0
  34. package/templates/apps/api/apps/{{projectName}}-service/tests/service.test.ts.hbs +3 -1
  35. package/templates/apps/api/e2e/api-service.spec.ts.hbs +4 -2
  36. package/templates/apps/api/{eslint.config.js → eslint.config.js.hbs} +6 -8
  37. package/templates/apps/api/package.json.hbs +3 -3
  38. package/templates/apps/api/pnpm-workspace.yaml.hbs +26 -0
  39. package/templates/apps/api/tsconfig.base.json +0 -1
  40. package/templates/apps/fullstack/.dockerignore.hbs +3 -0
  41. package/templates/apps/fullstack/.gitignore.hbs +16 -0
  42. package/templates/apps/fullstack/.npmrc.hbs +1 -0
  43. package/templates/apps/fullstack/Dockerfile.hbs +6 -5
  44. package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +2 -2
  45. package/templates/apps/fullstack/apps/{{projectName}}-app/capacitor.config.ts.hbs +1 -1
  46. package/templates/apps/fullstack/apps/{{projectName}}-app/eslint.config.js.hbs +5 -8
  47. package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +7 -2
  48. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/en-US.json.hbs +1 -34
  49. package/templates/apps/fullstack/apps/{{projectName}}-app/messages/zh-CN.json.hbs +1 -34
  50. package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +2 -2
  51. package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +30 -170
  52. package/templates/apps/fullstack/apps/{{projectName}}-app/src/app.d.ts.hbs +1 -1
  53. package/templates/apps/fullstack/apps/{{projectName}}-app/src/lib/capacitor.ts.hbs +6 -5
  54. package/templates/apps/fullstack/apps/{{projectName}}-app/src/main.ts.hbs +1 -1
  55. package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +48 -22
  56. package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +3 -1
  57. package/templates/apps/fullstack/apps/{{projectName}}-desktop/eslint.config.js.hbs +5 -8
  58. package/templates/apps/fullstack/apps/{{projectName}}-desktop/index.html.hbs +6 -1
  59. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/en-US.json.hbs +1 -28
  60. package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/zh-CN.json.hbs +1 -28
  61. package/templates/apps/fullstack/apps/{{projectName}}-desktop/package.json.hbs +5 -4
  62. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src/App.svelte.hbs +10 -141
  63. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/Cargo.toml.hbs +5 -5
  64. package/templates/apps/fullstack/apps/{{projectName}}-desktop/src-tauri/tauri.conf.json.hbs +3 -3
  65. package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +21 -2
  66. package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +2 -2
  67. package/templates/apps/fullstack/apps/{{projectName}}-web/eslint.config.js.hbs +5 -8
  68. package/templates/apps/fullstack/apps/{{projectName}}-web/index.html.hbs +6 -1
  69. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/en-US.json.hbs +1 -28
  70. package/templates/apps/fullstack/apps/{{projectName}}-web/messages/zh-CN.json.hbs +1 -28
  71. package/templates/apps/fullstack/apps/{{projectName}}-web/package.json.hbs +2 -2
  72. package/templates/apps/fullstack/apps/{{projectName}}-web/src/App.svelte.hbs +5 -143
  73. package/templates/apps/fullstack/apps/{{projectName}}-web/src/app.d.ts.hbs +1 -1
  74. package/templates/apps/fullstack/apps/{{projectName}}-web/src/main.ts.hbs +1 -1
  75. package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +48 -22
  76. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +207 -17
  77. package/templates/apps/fullstack/{eslint.config.js → eslint.config.js.hbs} +6 -8
  78. package/templates/apps/fullstack/package.json.hbs +3 -3
  79. package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +2 -0
  80. package/templates/apps/fullstack/packages/{{projectName}}-contract/package.json.hbs +1 -1
  81. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-contract.ts.hbs +66 -0
  82. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +96 -0
  83. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/app-schemas.ts.hbs +3 -0
  84. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +3 -1
  85. package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +5 -0
  86. package/templates/apps/fullstack/packages/{{projectName}}-contract/tests/contract.test.ts.hbs +3 -0
  87. package/templates/apps/fullstack/packages/{{projectName}}-contract/tsup.config.ts.hbs +1 -1
  88. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +13 -3
  89. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_audit.yml.hbs +3 -0
  90. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_cache.yml.hbs +2 -0
  91. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +10 -3
  92. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_db.yml.hbs +3 -0
  93. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +21 -0
  94. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +10 -3
  95. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_storage.yml.hbs +3 -0
  96. package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +10 -3
  97. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +63 -28
  98. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/index.ts.hbs +26 -11
  99. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +276 -0
  100. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +29 -0
  101. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +94 -0
  102. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/app-procedures.ts.hbs +6 -0
  103. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +5 -1
  104. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +98 -0
  105. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +446 -33
  106. package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/runtime.test.ts.hbs +48 -0
  107. package/templates/apps/fullstack/packages/{{projectName}}-serv/tsup.config.ts.hbs +1 -1
  108. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +19 -26
  109. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +78 -15
  110. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +78 -15
  111. package/templates/apps/fullstack/packages/{{projectName}}-shared/package.json.hbs +4 -1
  112. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/index.ts.hbs +3 -4
  113. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/api/api-client.ts.hbs +53 -27
  114. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +73 -0
  115. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +55 -45
  116. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AuditManagement.svelte.hbs +61 -0
  117. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +57 -0
  118. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/RoleManagement.svelte.hbs +49 -0
  119. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/UserManagement.svelte.hbs +174 -0
  120. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/management-store.svelte.ts.hbs +399 -0
  121. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/theme-store.svelte.ts.hbs +7 -39
  122. package/templates/apps/fullstack/packages/{{projectName}}-shared/svelte.config.js.hbs +1 -0
  123. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +151 -0
  124. package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/management-store.test.ts.hbs +101 -0
  125. package/templates/apps/fullstack/packages/{{projectName}}-shared/vitest.config.ts.hbs +7 -0
  126. package/templates/apps/fullstack/playwright.config.ts.hbs +6 -0
  127. package/templates/apps/fullstack/pnpm-workspace.yaml.hbs +24 -0
  128. package/templates/apps/fullstack/tsconfig.base.json +0 -1
  129. package/templates/apps/h5/src/routes/+layout.svelte.hbs +4 -0
  130. package/templates/apps/h5/src/routes/+page.svelte.hbs +3 -2
  131. package/templates/apps/mobile-app/index.html.hbs +2 -2
  132. package/templates/apps/mobile-app/src/App.svelte.hbs +6 -2
  133. package/templates/apps/mobile-app/src/lib/app-config.ts.hbs +13 -0
  134. package/templates/apps/website/src/routes/+layout.svelte.hbs +8 -2
  135. package/templates/apps/website/src/routes/+page.svelte.hbs +4 -3
  136. package/templates/base/.npmrc.hbs +1 -0
  137. package/templates/base/Dockerfile.hbs +9 -8
  138. package/templates/base/e2e/app.spec.ts.hbs +1 -0
  139. package/templates/base/eslint-rules.js +45 -0
  140. package/templates/base/eslint.config.js.hbs +6 -8
  141. package/templates/base/package.json.hbs +39 -42
  142. package/templates/base/pnpm-workspace.yaml.hbs +26 -0
  143. package/templates/base/src/app.d.ts.hbs +4 -0
  144. package/templates/base/src/hooks.server.ts.hbs +0 -8
  145. package/templates/base/src/lib/server/init.ts.hbs +26 -5
  146. package/templates/base/src/routes/+layout.server.ts.hbs +18 -0
  147. package/templates/base/tests/scaffold.test.ts.hbs +21 -1
  148. package/templates/base/vite.config.ts.hbs +21 -0
  149. package/templates/features/ai/routes/api/chat/+server.ts +6 -7
  150. package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +1 -1
  151. package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +1 -1
  152. package/templates/features/iam/routes-shared/api/auth/login/+server.ts +14 -10
  153. package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +6 -4
  154. package/templates/features/storage/routes/api/upload/+server.ts +10 -8
  155. package/templates/skills/AGENTS.md +9 -8
  156. package/templates/skills/CLAUDE.md +0 -10
  157. package/templates/skills/bridges/admin/AGENTS.md +8 -0
  158. package/templates/skills/bridges/admin/copilot-instructions.md +2 -0
  159. package/templates/skills/bridges/api/AGENTS.md +8 -1
  160. package/templates/skills/bridges/api/copilot-instructions.md +2 -0
  161. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +21 -4
  162. package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +5 -0
  163. package/templates/skills/bridges/h5/AGENTS.md +8 -0
  164. package/templates/skills/bridges/h5/copilot-instructions.md +2 -0
  165. package/templates/skills/bridges/mobile-app/AGENTS.md +8 -0
  166. package/templates/skills/bridges/mobile-app/copilot-instructions.md +2 -0
  167. package/templates/skills/bridges/website/AGENTS.md +8 -0
  168. package/templates/skills/bridges/website/copilot-instructions.md +2 -0
  169. package/templates/skills/copilot-instructions.md +2 -0
  170. package/templates/skills/hai-api-client/SKILL.md +2 -0
  171. package/templates/skills/hai-app-create/SKILL.md +9 -7
  172. package/templates/skills/hai-app-create/development.md +72 -0
  173. package/templates/skills/hai-app-create/reference.md +44 -32
  174. package/templates/skills/hai-app-review/SKILL.md +50 -144
  175. package/templates/skills/hai-app-tests/SKILL.md +3 -1
  176. package/templates/skills/hai-app-tests/reference.md +47 -27
  177. package/templates/skills/hai-build/SKILL.md +5 -4
  178. package/templates/skills/hai-build/reference.md +56 -34
  179. package/templates/skills/hai-ci/SKILL.md +8 -15
  180. package/templates/skills/hai-core/SKILL.md +2 -0
  181. package/templates/skills/hai-core/reference.md +6 -4
  182. package/templates/skills/hai-framework-sync/SKILL.md +8 -2
  183. package/templates/skills/hai-pr-review/SKILL.md +5 -4
  184. package/templates/skills/hai-reldb/SKILL.md +1 -0
  185. package/templates/skills/hai-reldb/reference.md +3 -0
  186. package/templates/skills/hai-scheduler/SKILL.md +3 -0
  187. package/templates/skills/hai-serv/SKILL.md +2 -0
  188. package/templates/skills/hai-serv/reference.md +18 -2
  189. package/templates/skills/hai-storage/SKILL.md +3 -1
  190. package/templates/skills/hai-ui/SKILL.md +6 -0
  191. package/templates/skills/hai-ui/reference.md +7 -4
  192. package/templates/skills/hai-vecdb/reference.md +53 -49
  193. package/dist/chunk-O3ANVICQ.js.map +0 -1
  194. package/dist/chunk-WUYLYCW2.js +0 -237
  195. package/dist/chunk-WUYLYCW2.js.map +0 -1
  196. package/dist/cli-config-templates-OWBEGBZQ.js +0 -5
  197. package/dist/src-TBIET7YU.js.map +0 -1
  198. package/templates/apps/fullstack/apps/{{projectName}}-app/tests/api.test.ts.hbs +0 -28
  199. package/templates/apps/fullstack/apps/{{projectName}}-desktop/tests/api.test.ts.hbs +0 -28
  200. package/templates/apps/fullstack/apps/{{projectName}}-web/tests/api.test.ts.hbs +0 -55
  201. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/LanguageSwitcher.svelte.hbs +0 -27
  202. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ThemeSwitcher.svelte.hbs +0 -18
  203. package/templates/skills/bridges/admin/CLAUDE.md +0 -11
  204. package/templates/skills/bridges/api/CLAUDE.md +0 -11
  205. package/templates/skills/bridges/fullstack/CLAUDE.md.hbs +0 -5
  206. package/templates/skills/bridges/h5/CLAUDE.md +0 -11
  207. package/templates/skills/bridges/mobile-app/CLAUDE.md +0 -11
  208. package/templates/skills/bridges/website/CLAUDE.md +0 -11
@@ -1,32 +1,58 @@
1
1
  {{! @skipIfEmpty }}
2
2
  {{#if fullstack.frontends.web}}
3
+ import process from 'node:process'
4
+ import { fileURLToPath } from 'node:url'
3
5
  import { haiOptimizeExclude, haiPrebundledDeps } from '@h-ai/ui/vite'
4
6
  import { paraglideVitePlugin } from '@inlang/paraglide-js'
5
7
  import { svelte } from '@sveltejs/vite-plugin-svelte'
6
8
  import tailwindcss from '@tailwindcss/vite'
9
+ import { loadEnv } from 'vite'
7
10
  import { defineConfig } from 'vitest/config'
8
11
 
9
- export default defineConfig({
10
- plugins: [
11
- svelte(),
12
- paraglideVitePlugin({
13
- project: './project.inlang',
14
- outdir: './src/lib/paraglide',
15
- strategy: ['cookie', 'baseLocale'],
16
- }),
17
- tailwindcss(),
18
- ],
19
- optimizeDeps: {
20
- exclude: [...haiOptimizeExclude],
21
- include: [...haiPrebundledDeps],
22
- },
23
- envPrefix: ['VITE_', 'PUBLIC_'],
24
- ssr: {
25
- noExternal: [/@h-ai\//, /{{fullstack.sharedPackageName}}/],
26
- },
27
- test: {
28
- environment: 'node',
29
- include: ['tests/**/*.test.ts'],
30
- },
12
+ const envDir = fileURLToPath(new URL('../../', import.meta.url))
13
+
14
+ export default defineConfig(({ mode }) => {
15
+ const env = loadEnv(mode, envDir, 'PUBLIC_')
16
+ const target = (env.PUBLIC_API_BASE || process.env.SERVICE_BASE_URL || 'http://127.0.0.1:3000').replace(/\/api\/v1\/?$/, '')
17
+ const proxy = { '/api': { target, changeOrigin: true } }
18
+ return {
19
+ envDir,
20
+ server: { proxy },
21
+ preview: { proxy },
22
+ build: {
23
+ rolldownOptions: {
24
+ output: {
25
+ codeSplitting: {
26
+ groups: [
27
+ { name: 'vendor-ui', test: /node_modules\/(?:@h-ai\/ui|bits-ui|svelte)\// },
28
+ { name: 'vendor-crypto', test: /node_modules\/(?:@h-ai\/(?:core|crypto)|sm-crypto(?:-v2)?)\// },
29
+ { name: 'vendor-api', test: /node_modules\/(?:@orpc\/|zod\/)/ },
30
+ ],
31
+ },
32
+ },
33
+ },
34
+ },
35
+ plugins: [
36
+ svelte(),
37
+ paraglideVitePlugin({
38
+ project: './project.inlang',
39
+ outdir: './src/lib/paraglide',
40
+ strategy: ['cookie', 'baseLocale'],
41
+ }),
42
+ tailwindcss(),
43
+ ],
44
+ optimizeDeps: {
45
+ exclude: [...haiOptimizeExclude],
46
+ include: [...haiPrebundledDeps],
47
+ },
48
+ envPrefix: ['VITE_', 'PUBLIC_'],
49
+ ssr: {
50
+ noExternal: [/@h-ai\//, /{{fullstack.sharedPackageName}}/],
51
+ },
52
+ test: {
53
+ environment: 'node',
54
+ include: ['tests/**/*.test.ts'],
55
+ },
56
+ }
31
57
  })
32
58
  {{/if}}
@@ -1,33 +1,223 @@
1
- import { expect, test } from '@playwright/test'
1
+ import type { Page } from '@playwright/test'
2
+ import { Buffer } from 'node:buffer'
3
+ import { randomUUID } from 'node:crypto'
4
+ import process from 'node:process'
2
5
  import { apiContract } from '@h-ai/api-contract'
6
+ import { expect, test } from '@playwright/test'
3
7
  import { {{fullstack.contractExportName}} } from '{{fullstack.contractPackageName}}'
4
8
 
5
- const DEFAULT_SERVICE_BASE_URL = 'http://127.0.0.1:3000'
6
- const apiBaseUrl = process.env.SERVICE_BASE_URL || DEFAULT_SERVICE_BASE_URL
9
+ const apiBaseUrl = process.env.SERVICE_BASE_URL || 'http://127.0.0.1:3000'
7
10
  const apiPrefix = '/api/v1'
11
+ const adminPassword = process.env.E2E_ADMIN_PASSWORD || 'Scaffold-test-Admin123!'
12
+ const configuredName = process.env.HAI_CORE_NAME || 'Configured scaffold'
13
+ const configuredVersion = process.env.HAI_CORE_VERSION || '9.8.7'
14
+ const configuredEnv = process.env.HAI_CORE_ENV || 'test'
15
+
16
+ // 测试专用的一像素 PNG;上传仍使用完整前后端加密链路。
17
+ const avatar = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aSxkAAAAASUVORK5CYII=', 'base64')
18
+
19
+ async function login(page: Page, username: string, password: string) {
20
+ await expect(page.getByTestId('login-form')).toBeVisible()
21
+ await page.locator('#login-username').fill(username)
22
+ await page.getByTestId('login-form').locator('input[type="password"]').fill(password)
23
+ await page.getByTestId('login-form').getByRole('button', { name: /登录|Sign in|Login|Log in/i }).click()
24
+ }
25
+
26
+ async function userEditor(page: Page, username: string) {
27
+ await page.getByTestId('users-search').locator('input').fill(username)
28
+ await page.getByTestId('users-page').getByRole('button', { name: /查询|Search/ }).click()
29
+ const row = page.getByRole('row').filter({ hasText: username })
30
+ await expect(row).toBeVisible()
31
+ await row.getByRole('button', { name: /编辑|Edit/, exact: true }).click()
32
+ return row
33
+ }
8
34
 
9
35
  test('service health endpoint is ready', async ({ request }) => {
10
36
  const response = await request.get(`${apiBaseUrl}/health`)
11
- expect(response.ok()).toBe(true)
12
37
  await expect(response).toBeOK()
13
38
  })
14
39
 
15
- test('service echo endpoint returns HaiResult', async ({ request }) => {
16
- const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, {
17
- data: { message: 'hello e2e' },
18
- })
19
- expect(response.ok()).toBe(true)
20
-
21
- const body = await response.json()
22
- expect(body.success).toBe(true)
23
- expect(body.data.message).toBe('hello e2e')
24
- expect(body.data.requestId).toBeTruthy()
40
+ test('service rejects plaintext business requests', async ({ request }) => {
41
+ const response = await request.post(`${apiBaseUrl}${apiPrefix}${apiContract.pathOf({{fullstack.contractExportName}}.app.echo)}`, { data: { message: 'hello e2e' } })
42
+ expect(response.status()).toBe(400)
25
43
  })
44
+
26
45
  {{#if fullstack.e2eFrontend}}
27
46
 
28
- test('frontend loads service status from page', async ({ page }) => {
47
+ test('frontend displays configured core metadata and restores the administrator session', async ({ page }) => {
29
48
  await page.goto('/')
30
- await expect(page.getByRole('heading', { level: 1 })).toBeVisible()
31
- await expect(page.getByTestId('service-status')).toContainText('{{fullstack.servPackageName}}')
49
+ await expect(page.getByTestId('app-name')).toHaveText(configuredName)
50
+ await expect(page).toHaveTitle(configuredName)
51
+ await expect(page.getByTestId('app-version')).toHaveText(`v${configuredVersion}`)
52
+ await expect(page.getByTestId('app-environment')).toHaveText(configuredEnv)
53
+ await login(page, 'admin', adminPassword)
54
+ await expect(page.getByTestId('users-page')).toBeVisible()
55
+ await page.reload()
56
+ await expect(page.getByTestId('users-page')).toBeVisible()
57
+ await expect(page.getByTestId('nav-roles')).toBeVisible()
58
+ await expect(page.getByTestId('nav-audit')).toBeVisible()
59
+ await page.getByTestId('logout').click()
60
+ await expect(page.getByTestId('login-form')).toBeVisible()
61
+ })
62
+
63
+ test('audit filters align and fit desktop, tablet and narrow mobile screens', async ({ page }) => {
64
+ await page.goto('/')
65
+ await login(page, 'admin', adminPassword)
66
+ await page.getByTestId('nav-audit').click()
67
+ await expect(page.getByTestId('audit-page')).toBeVisible()
68
+ for (const width of [1440, 960, 390, 320]) {
69
+ await page.setViewportSize({ width, height: 900 })
70
+ const controls = await page.getByTestId('audit-page').locator('form').evaluate(form => [
71
+ form.querySelector('#audit-action')?.parentElement,
72
+ form.querySelector('#audit-resource')?.parentElement,
73
+ form.querySelector('#audit-start'),
74
+ form.querySelector('#audit-end'),
75
+ form.querySelector('button[type="submit"]'),
76
+ ].map((element) => {
77
+ if (!element)
78
+ throw new Error('Audit filter control is missing')
79
+ const rect = element.getBoundingClientRect()
80
+ return { top: rect.top, bottom: rect.bottom, height: rect.height }
81
+ }))
82
+ for (const control of controls)
83
+ expect(control.height).toBeCloseTo(40, 0)
84
+ if (width >= 1280) {
85
+ expect(Math.max(...controls.map(control => control.top)) - Math.min(...controls.map(control => control.top))).toBeLessThan(1)
86
+ expect(Math.max(...controls.map(control => control.bottom)) - Math.min(...controls.map(control => control.bottom))).toBeLessThan(1)
87
+ }
88
+ else if (width >= 640) {
89
+ expect(controls[0]!.top).toBeCloseTo(controls[1]!.top, 0)
90
+ expect(controls[2]!.top).toBeCloseTo(controls[3]!.top, 0)
91
+ }
92
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
93
+ }
94
+ })
95
+
96
+ test('administrator and user complete account, avatar, preference, password and audit workflows', async ({ page, browser }) => {
97
+ test.setTimeout(120_000)
98
+ const username = `member-${randomUUID().slice(0, 8)}`
99
+ const password = 'User-test-Pass123!'
100
+ const newPassword = 'Changed-test-Pass123!'
101
+ const encryptedRequests: string[] = []
102
+ const encryptedResponses: string[] = []
103
+ page.on('request', (request) => {
104
+ if (request.url().includes(`${apiPrefix}/`) && !request.url().includes('/_hai/') && request.postData()) {
105
+ expect(request.headers()['x-encrypted']).toBe('true')
106
+ expect(request.postData()).not.toContain('"password"')
107
+ expect(request.postData()).not.toContain(adminPassword)
108
+ encryptedRequests.push(request.url())
109
+ }
110
+ })
111
+ page.on('response', (response) => {
112
+ if (response.request().method() !== 'OPTIONS' && response.url().includes(`${apiPrefix}/`) && !response.url().includes('/_hai/') && !response.url().includes('/assets/')) {
113
+ expect(response.headers()['x-encrypted']).toBe('true')
114
+ encryptedResponses.push(response.url())
115
+ }
116
+ })
117
+ await page.goto('/')
118
+ await login(page, 'admin', adminPassword)
119
+ await expect(page.getByTestId('users-page')).toBeVisible()
120
+ await page.getByTestId('create-user').click()
121
+ await expect(page.getByRole('dialog', { name: /新增用户|New user|Create user|Add user/ })).toBeVisible()
122
+ await expect(page.getByTestId('user-username').locator('input')).toBeFocused()
123
+ await page.getByTestId('user-username').locator('input').fill(username)
124
+ await page.getByTestId('user-display-name').locator('input').fill('Test member')
125
+ await page.getByTestId('user-email').locator('input').fill(`${username}@example.com`)
126
+ await page.getByTestId('user-password').locator('input').fill(password)
127
+ await page.getByTestId('user-save').click()
128
+ await expect(page.getByTestId('user-editor')).not.toBeVisible()
129
+ const userRow = page.getByRole('row').filter({ hasText: username })
130
+ await expect(userRow).toBeVisible()
131
+ await userRow.getByTestId('user-avatar-input').setInputFiles({ name: 'avatar.png', mimeType: 'image/png', buffer: avatar })
132
+ await expect(userRow.locator('img')).toHaveAttribute('src', /\/assets\/avatars\//)
133
+ await expect.poll(() => userRow.locator('img').evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true)
134
+ await page.getByTestId('nav-roles').click()
135
+ await expect(page.getByTestId('roles-page')).toBeVisible()
136
+ await page.getByTestId('roles-page').getByRole('button', { name: /编辑说明|Edit description/ }).first().click()
137
+ await page.getByTestId('role-description').locator('textarea').fill('Workspace administration')
138
+ await page.getByRole('dialog').getByRole('button', { name: /保存|Save/, exact: true }).focus()
139
+ await page.keyboard.press('Tab')
140
+ await expect(page.getByRole('dialog').getByRole('button', { name: /关闭|Close/, exact: true })).toBeFocused()
141
+ await page.keyboard.press('Shift+Tab')
142
+ await expect(page.getByRole('dialog').getByRole('button', { name: /保存|Save/, exact: true })).toBeFocused()
143
+ await page.getByRole('dialog').getByRole('button', { name: /保存|Save/, exact: true }).click()
144
+ await expect(page.getByTestId('roles-page')).toContainText('Workspace administration')
145
+
146
+ const userContext = await browser.newContext()
147
+ const userPage = await userContext.newPage()
148
+ try {
149
+ await userPage.goto('/')
150
+ await login(userPage, username, password)
151
+ await expect(userPage.getByTestId('settings-page')).toBeVisible()
152
+ await expect(userPage.getByTestId('nav-users')).toHaveCount(0)
153
+ await expect(userPage.getByTestId('nav-roles')).toHaveCount(0)
154
+ await expect(userPage.getByTestId('nav-audit')).toHaveCount(0)
155
+ await userPage.getByTestId('profile-form').locator('input[name="displayName"]').fill('Updated member')
156
+ await userPage.getByTestId('profile-form').locator('input[name="email"]').fill('')
157
+ await userPage.getByTestId('profile-form').getByRole('button', { name: /保存|Save/, exact: true }).click()
158
+ await expect(userPage.getByTestId('notice')).toBeVisible()
159
+ await userPage.getByTestId('profile-form').locator('input[type="file"]').setInputFiles({ name: 'personal.png', mimeType: 'image/png', buffer: avatar })
160
+ await expect(userPage.getByTestId('notice')).toHaveText(/头像已更新|Avatar updated/)
161
+ await userPage.getByTestId('profile-language').click()
162
+ await userPage.getByRole('option', { name: 'English', exact: true }).click()
163
+ await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
164
+ await expect(userPage.locator('html')).toHaveAttribute('lang', 'en-US')
165
+ const themes = [{{#each fullstack.theme.themes}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]
166
+ const selectedTheme = themes.find(theme => theme !== '{{fullstack.theme.defaultTheme}}') ?? themes[0]
167
+ await userPage.getByTestId('profile-theme').click()
168
+ await userPage.getByRole('option').nth(themes.indexOf(selectedTheme)).click()
169
+ await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
170
+ await userPage.reload()
171
+ await expect(userPage.getByTestId('settings-page')).toBeVisible()
172
+ await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
173
+ await expect(userPage.locator('html')).toHaveAttribute('data-theme', selectedTheme)
174
+ await expect(userPage.getByTestId('profile-form').locator('input[name="displayName"]')).toHaveValue('Updated member')
175
+ await expect(userPage.getByTestId('profile-form').locator('input[name="email"]')).toHaveValue('')
176
+ await expect.poll(() => userPage.getByTestId('profile-form').locator('img').evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true)
177
+ await userPage.setViewportSize({ width: 390, height: 844 })
178
+ await expect.poll(() => userPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
179
+ await userPage.getByTestId('mobile-menu').click()
180
+ await expect(userPage.getByTestId('nav-settings')).toBeVisible()
181
+ await userPage.getByTestId('nav-settings').click()
182
+ const passwordInputs = userPage.getByTestId('password-form').locator('input[type="password"]')
183
+ await passwordInputs.nth(0).fill(password)
184
+ await passwordInputs.nth(1).fill(password)
185
+ await passwordInputs.nth(2).fill(password)
186
+ await userPage.getByTestId('password-form').getByRole('button', { name: /Change password/i }).click()
187
+ await expect(userPage.getByTestId('settings-page')).toBeVisible()
188
+ await expect(userPage.getByTestId('error')).toContainText(/检查|check/i)
189
+ await passwordInputs.nth(1).fill(newPassword)
190
+ await passwordInputs.nth(2).fill(newPassword)
191
+ await userPage.getByTestId('password-form').getByRole('button', { name: /Change password/i }).click()
192
+ await expect(userPage.getByTestId('login-form')).toBeVisible()
193
+ await login(userPage, username, password)
194
+ await expect(userPage.getByTestId('login-form')).toContainText(/不正确|incorrect/)
195
+ await login(userPage, username, newPassword)
196
+ await expect(userPage.getByTestId('settings-page')).toBeVisible()
197
+
198
+ await page.getByTestId('nav-users').click()
199
+ await userEditor(page, username)
200
+ await page.getByTestId('user-editor').locator('input[type="checkbox"]').uncheck()
201
+ await page.getByTestId('user-save').click()
202
+ await expect(page.getByTestId('user-editor')).not.toBeVisible()
203
+ await userPage.getByTestId('profile-form').getByRole('button', { name: 'Save', exact: true }).click()
204
+ await expect(userPage.getByTestId('login-form')).toBeVisible()
205
+ await expect(userPage.getByTestId('login-form')).toContainText(/expired|过期/)
206
+ }
207
+ finally {
208
+ await userContext.close()
209
+ }
210
+
211
+ await page.getByRole('row').filter({ hasText: username }).getByRole('button', { name: /删除|Delete/, exact: true }).click()
212
+ await page.getByTestId('confirm-delete-user').click()
213
+ await expect(page.getByRole('row').filter({ hasText: username })).toHaveCount(0)
214
+ await page.getByTestId('nav-audit').click()
215
+ await page.getByTestId('audit-resource').locator('input').fill('users')
216
+ await page.getByTestId('audit-page').getByRole('button', { name: /查询|Search/ }).click()
217
+ await expect(page.getByTestId('audit-page')).toContainText('delete')
218
+ await page.getByTestId('audit-page').getByRole('button', { name: /详情|Details/ }).first().click()
219
+ await expect(page.getByRole('dialog')).toContainText(/users/)
220
+ expect(encryptedRequests.length).toBeGreaterThan(5)
221
+ expect(encryptedResponses.length).toBeGreaterThan(5)
32
222
  })
33
223
  {{/if}}
@@ -1,6 +1,9 @@
1
- import antfu from '@antfu/eslint-config'
1
+ import antfu, { GLOB_SRC } from '@antfu/eslint-config'
2
+ import { haiQualityConfig } from './eslint-rules.js'
2
3
 
3
4
  export default antfu({
5
+ // unused-imports 使用 TS 规则,JS 同样使用 TS parser 以保持 scope 契约一致。
6
+ typescript: { files: [GLOB_SRC] },
4
7
  formatters: true,
5
8
  ignores: [
6
9
  '**/build/**',
@@ -9,6 +12,7 @@ export default antfu({
9
12
  '**/project.inlang/**',
10
13
  '**/*.d.ts',
11
14
  '**/.agents/**',
15
+ '**/.claude/**',
12
16
  '**/.github/**',
13
17
  '**/AGENTS.md',
14
18
  '**/CLAUDE.md',
@@ -17,13 +21,7 @@ export default antfu({
17
21
  '**/package.json',
18
22
  ],
19
23
  svelte: true,
20
- }, {
21
- files: ['**/*.ts', '**/*.svelte'],
22
- rules: {
23
- 'no-console': 'error',
24
- 'ts/no-explicit-any': 'error',
25
- },
26
- }, {
24
+ }, haiQualityConfig, {
27
25
  files: ['**/*.svelte'],
28
26
  rules: {
29
27
  'svelte/html-quotes': 'off',
@@ -1,10 +1,10 @@
1
1
  {
2
- "engines": { "node": ">=22.12.0" },
2
+ "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
3
3
  "name": "{{projectName}}",
4
4
  "type": "module",
5
5
  "version": "0.1.0",
6
6
  "private": true,
7
- "packageManager": "pnpm@9.15.0",
7
+ "packageManager": "pnpm@12.11.2",
8
8
  "description": "Full-stack hai workspace with separated contract, service and frontends.",
9
9
  "scripts": {
10
10
  "i18n:compile": "pnpm --filter {{fullstack.sharedPackageName}} paraglide:compile{{#each fullstack.frontendApps}} && pnpm --filter {{packageName}} paraglide:compile{{/each}}",
@@ -21,7 +21,7 @@
21
21
  "package": "pnpm build{{#if fullstack.frontends.web}} && pnpm --filter {{projectName}}-web package{{/if}}{{#if fullstack.frontends.app}} && pnpm --filter {{projectName}}-app package{{/if}}{{#if fullstack.frontends.desktop}} && pnpm --filter {{projectName}}-desktop package{{/if}}",
22
22
  "deploy": "pnpm package",
23
23
  "typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
24
- "lint": "pnpm -r --if-present lint",
24
+ "lint": "eslint *.js *.ts e2e && pnpm -r --if-present lint",
25
25
  "test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present test",
26
26
  "test:e2e": "pnpm build && playwright test",
27
27
  "clean": "pnpm -r --if-present clean && rimraf node_modules .turbo"
@@ -6,10 +6,12 @@
6
6
 
7
7
  - `src/app-schemas.ts`:跨接口、跨层复用的输入/数据 Schema 与类型。
8
8
  - `src/app-contract.ts`:app 领域 contract;每个业务 path 直接写在对应接口定义中。
9
+ - `src/admin-schemas.ts` / `src/admin-contract.ts`:管理员用户、固定角色、分页审计、个人设置、加密 JSON 头像上传 Schema/类型/路由;认证复用框架的 IAM auth contract,未开放自助注册和其它认证方式。
9
10
  - `src/{{projectName}}-contract.ts`:工程级 contract,供 `@h-ai/serv` 与 `@h-ai/api-client` 共用。
10
11
 
11
12
  只使用一次的输出 Schema 直接内联到 `.output(...)`;同一 contract 文件内重复使用时才保留私有常量。
12
13
  API 前缀、健康检查与跨端 Header 等传输配置位于 serv 包的 `config/_serv.yml`。
14
+ `SUPPORTED_LOCALES` 为生成时实际编译的语言列表;`app.info` 中名称、版本、环境、默认语言来自后端 core 配置。更新公开契约时同时更新 serv、shared typed API client、各端 UI 与测试。
13
15
 
14
16
  ## 开发
15
17
 
@@ -1,5 +1,5 @@
1
1
  {
2
- "engines": { "node": ">=22.12.0" },
2
+ "engines": { "node": "^22.22.2 || ^24.15.0 || >=26.0.0" },
3
3
  "name": "{{fullstack.contractPackageName}}",
4
4
  "type": "module",
5
5
  "version": "0.1.0",
@@ -0,0 +1,66 @@
1
+ import { apiContract } from '@h-ai/api-contract'
2
+ import { z } from 'zod'
3
+ import {
4
+ AdminAuditInputSchema,
5
+ AdminAuditSchema,
6
+ AdminCreateUserSchema,
7
+ AdminRoleSchema,
8
+ AdminUpdateUserSchema,
9
+ AdminUserSchema,
10
+ AdminUsersInputSchema,
11
+ AdminUsersSchema,
12
+ AvatarOutputSchema,
13
+ AvatarUploadSchema,
14
+ ProfileUpdateSchema,
15
+ } from './admin-schemas.js'
16
+
17
+ /** mini 管理业务路径统一定义;仅开放两种固定角色。 */
18
+ export const adminContract = {
19
+ users: {
20
+ list: apiContract.route({ method: 'POST', path: '/admin/users/list', tags: ['admin'] })
21
+ .input(AdminUsersInputSchema)
22
+ .output(apiContract.haiResultSchema(AdminUsersSchema)),
23
+ create: apiContract.route({ method: 'POST', path: '/admin/users/create', tags: ['admin'] })
24
+ .input(AdminCreateUserSchema)
25
+ .output(apiContract.haiResultSchema(AdminUserSchema)),
26
+ update: apiContract.route({ method: 'POST', path: '/admin/users/update', tags: ['admin'] })
27
+ .input(AdminUpdateUserSchema)
28
+ .output(apiContract.haiResultSchema(AdminUserSchema)),
29
+ delete: apiContract.route({ method: 'POST', path: '/admin/users/delete', tags: ['admin'] })
30
+ .input(z.object({ id: z.string().min(1) }).strict())
31
+ .output(apiContract.haiResultSchema(z.object({ deleted: z.literal(true) }))),
32
+ avatar: apiContract.route({ method: 'POST', path: '/admin/users/avatar', tags: ['admin'] })
33
+ .input(AvatarUploadSchema.extend({ id: z.string().min(1) }))
34
+ .output(apiContract.haiResultSchema(AvatarOutputSchema)),
35
+ },
36
+ roles: {
37
+ list: apiContract.route({ method: 'POST', path: '/admin/roles/list', tags: ['admin'] })
38
+ .output(apiContract.haiResultSchema(z.object({ items: z.array(AdminRoleSchema) }))),
39
+ update: apiContract.route({ method: 'POST', path: '/admin/roles/update', tags: ['admin'] })
40
+ .input(z.object({ id: z.string().min(1), description: z.string().max(300) }).strict())
41
+ .output(apiContract.haiResultSchema(AdminRoleSchema)),
42
+ },
43
+ audit: {
44
+ list: apiContract.route({ method: 'POST', path: '/admin/audit/list', tags: ['admin'] })
45
+ .input(AdminAuditInputSchema)
46
+ .output(apiContract.haiResultSchema(AdminAuditSchema)),
47
+ },
48
+ }
49
+
50
+ export const profileContract = {
51
+ update: apiContract.route({ method: 'POST', path: '/profile/update', tags: ['profile'] })
52
+ .input(ProfileUpdateSchema)
53
+ .output(apiContract.haiResultSchema(AdminUserSchema)),
54
+ avatar: apiContract.route({ method: 'POST', path: '/profile/avatar', tags: ['profile'] })
55
+ .input(AvatarUploadSchema)
56
+ .output(apiContract.haiResultSchema(AvatarOutputSchema)),
57
+ }
58
+
59
+ /** 复用框架认证契约,避免额外开放自助注册和未配置的认证方式。 */
60
+ export const authContract = {
61
+ login: apiContract.iam.auth.login,
62
+ logout: apiContract.iam.auth.logout,
63
+ currentUser: apiContract.iam.auth.currentUser,
64
+ refresh: apiContract.iam.auth.refresh,
65
+ changePassword: apiContract.iam.auth.changePassword,
66
+ }
@@ -0,0 +1,96 @@
1
+ import { z } from 'zod'
2
+
3
+ export const AdminRoleCodeSchema = z.enum(['admin', 'user'])
4
+ export const AdminPageInputSchema = z.object({
5
+ page: z.number().int().min(1).default(1),
6
+ pageSize: z.number().int().min(1).max(100).default(20),
7
+ })
8
+ export const AdminUserSchema = z.object({
9
+ id: z.string(),
10
+ username: z.string(),
11
+ email: z.string().optional(),
12
+ displayName: z.string().optional(),
13
+ avatarUrl: z.string().optional(),
14
+ enabled: z.boolean(),
15
+ roles: z.array(AdminRoleCodeSchema),
16
+ createdAt: z.string().datetime(),
17
+ updatedAt: z.string().datetime(),
18
+ })
19
+ export const AdminUsersInputSchema = AdminPageInputSchema.extend({
20
+ search: z.string().max(100).optional(),
21
+ enabled: z.boolean().optional(),
22
+ })
23
+ export const AdminUsersSchema = z.object({
24
+ items: z.array(AdminUserSchema),
25
+ total: z.number(),
26
+ page: z.number(),
27
+ pageSize: z.number(),
28
+ })
29
+ export const AdminCreateUserSchema = z.object({
30
+ username: z.string().min(3).max(64).regex(/^[\w-]+$/),
31
+ password: z.string().min(8).max(128),
32
+ email: z.email().optional(),
33
+ displayName: z.string().max(100).optional(),
34
+ role: AdminRoleCodeSchema.default('user'),
35
+ enabled: z.boolean().default(true),
36
+ }).strict()
37
+ export const AdminUpdateUserSchema = z.object({
38
+ id: z.string().min(1),
39
+ email: z.union([z.email(), z.literal('')]).optional(),
40
+ displayName: z.string().max(100).optional(),
41
+ enabled: z.boolean().optional(),
42
+ role: AdminRoleCodeSchema.optional(),
43
+ }).strict()
44
+ export const AdminRoleSchema = z.object({
45
+ id: z.string(),
46
+ code: AdminRoleCodeSchema,
47
+ name: z.string(),
48
+ description: z.string().optional(),
49
+ userCount: z.number(),
50
+ })
51
+ export const AdminAuditInputSchema = AdminPageInputSchema.extend({
52
+ userId: z.string().max(100).optional(),
53
+ action: z.string().max(50).optional(),
54
+ resource: z.string().max(50).optional(),
55
+ startDate: z.string().datetime().optional(),
56
+ endDate: z.string().datetime().optional(),
57
+ })
58
+ export const AdminAuditLogSchema = z.object({
59
+ id: z.string(),
60
+ userId: z.string().nullable(),
61
+ username: z.string().nullable(),
62
+ action: z.string(),
63
+ resource: z.string(),
64
+ resourceId: z.string().nullable(),
65
+ details: z.string().nullable(),
66
+ ipAddress: z.string().nullable(),
67
+ userAgent: z.string().nullable(),
68
+ createdAt: z.string().datetime(),
69
+ })
70
+ export const AdminAuditSchema = z.object({
71
+ items: z.array(AdminAuditLogSchema),
72
+ total: z.number(),
73
+ page: z.number(),
74
+ pageSize: z.number(),
75
+ })
76
+ export const ProfileUpdateSchema = z.object({
77
+ displayName: z.string().max(100).optional(),
78
+ email: z.union([z.email(), z.literal('')]).optional(),
79
+ locale: z.enum(['zh-CN', 'en-US']).optional(),
80
+ theme: z.string().min(1).max(40).regex(/^[\w-]+$/).optional(),
81
+ }).strict()
82
+ /** 512 KiB 头像以 JSON 传输,上传及响应均经过统一加密。 */
83
+ export const AvatarUploadSchema = z.object({
84
+ contentType: z.enum(['image/png', 'image/jpeg', 'image/webp']),
85
+ base64: z.string().min(1).max(699052).regex(/^[A-Z\d+/]+={0,2}$/i),
86
+ }).strict()
87
+ export const AvatarOutputSchema = z.object({ avatarUrl: z.string().describe('Generated relative asset path') })
88
+
89
+ export type AdminUser = z.infer<typeof AdminUserSchema>
90
+ export type AdminRole = z.infer<typeof AdminRoleSchema>
91
+ export type AdminRoleCode = z.infer<typeof AdminRoleCodeSchema>
92
+ export type AdminAuditLog = z.infer<typeof AdminAuditLogSchema>
93
+ export type AdminCreateUser = z.infer<typeof AdminCreateUserSchema>
94
+ export type AdminUpdateUser = z.infer<typeof AdminUpdateUserSchema>
95
+ export type ProfileUpdate = z.infer<typeof ProfileUpdateSchema>
96
+ export type AvatarUpload = z.infer<typeof AvatarUploadSchema>
@@ -7,6 +7,9 @@ export const AppFrontendTargetSchema = z.enum(['web', 'app', 'miniapp', 'desktop
7
7
  export const AppInfoOutputDataSchema = z.object({
8
8
  name: z.string(),
9
9
  version: z.string(),
10
+ env: z.string(),
11
+ defaultLocale: z.string(),
12
+ supportedLocales: z.array(z.string()),
10
13
  startedAt: z.string().datetime(),
11
14
  frontends: z.array(AppFrontendTargetSchema),
12
15
  })
@@ -1,3 +1,5 @@
1
1
  export * from './app-contract.js'
2
2
  export * from './app-schemas.js'
3
- export * from './{{projectName}}-contract.js'
3
+ export * from './admin-contract.js'
4
+ export * from './admin-schemas.js'
5
+ export * from './{{fullstack.contractPackageName}}.js'
@@ -1,12 +1,17 @@
1
1
  import { apiContract } from '@h-ai/api-contract'
2
+ import { adminContract, authContract, profileContract } from './admin-contract.js'
2
3
  import { appContract } from './app-contract.js'
3
4
 
4
5
  /** 当前工程启用的前端目标。 */
5
6
  export const ENABLED_FRONTENDS = {{{fullstack.selectedFrontendsJson}}} as const
7
+ export const SUPPORTED_LOCALES = [{{#each fullstack.i18n.locales}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}] as const
6
8
 
7
9
  /** 工程级 API contract,前端 typed client 与后端 service 共用。 */
8
10
  export const {{fullstack.contractExportName}} = apiContract.create({
9
11
  app: appContract,
12
+ iam: { auth: authContract },
13
+ admin: adminContract,
14
+ profile: profileContract,
10
15
  })
11
16
 
12
17
  export type FullstackContract = typeof {{fullstack.contractExportName}}
@@ -19,6 +19,9 @@ describe('{{fullstack.contractPackageName}}', () => {
19
19
  const result = AppInfoOutputDataSchema.safeParse({
20
20
  name: '{{fullstack.servPackageName}}',
21
21
  version: '0.1.0',
22
+ env: 'test',
23
+ defaultLocale: 'zh-CN',
24
+ supportedLocales: ['zh-CN', 'en-US'],
22
25
  startedAt: '2026-01-01T00:00:00.000Z',
23
26
  frontends: [...ENABLED_FRONTENDS],
24
27
  })
@@ -3,7 +3,7 @@ import { defineConfig } from 'tsup'
3
3
  export default defineConfig({
4
4
  entry: ['src/index.ts'],
5
5
  format: ['esm'],
6
- dts: true,
6
+ dts: { compilerOptions: { ignoreDeprecations: '6.0' } },
7
7
  sourcemap: true,
8
8
  clean: true,
9
9
  })