@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.68
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.
- package/README.en.md +86 -241
- package/README.ja.md +86 -241
- package/README.md +86 -241
- package/dist/{src-RDQ2EIBT.js → chunk-FI56GONY.js} +32 -2669
- package/dist/chunk-FI56GONY.js.map +1 -0
- package/dist/chunk-MBPB37FR.js +1348 -0
- package/dist/chunk-MBPB37FR.js.map +1 -0
- package/dist/{chunk-QYX6V7H2.js → chunk-ZFNBUF77.js} +347 -276
- package/dist/chunk-ZFNBUF77.js.map +1 -0
- package/dist/cli-config-templates-W2LLPMB6.js +5 -0
- package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-W2LLPMB6.js.map} +1 -1
- package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-US5CCAJ4.js} +3 -3
- package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-US5CCAJ4.js.map} +1 -1
- package/dist/index.js +1304 -532
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -0
- package/dist/src-DSJJXM2F.js +3849 -0
- package/dist/src-DSJJXM2F.js.map +1 -0
- package/package.json +9 -8
- package/templates/README.md +48 -145
- package/templates/apps/admin/messages/ja-JP.json +38 -0
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +16 -6
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +3 -3
- package/templates/apps/api/.dockerignore.hbs +14 -7
- package/templates/apps/api/.gitignore.hbs +7 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/api/apps/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/api/apps/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +4 -1
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +33 -8
- package/templates/apps/api/apps/{{projectName}}-service/config/_core.yml.hbs +0 -10
- package/templates/apps/api/apps/{{projectName}}-service/config/_serv.yml.hbs +2 -1
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
- package/templates/apps/api/apps/{{projectName}}-service/src/app.ts.hbs +24 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/index.ts.hbs +8 -1
- package/templates/apps/api/apps/{{projectName}}-service/src/lib/server/init.ts.hbs +68 -11
- package/templates/apps/api/apps/{{projectName}}-service/src/server/procedures/index.ts.hbs +10 -2
- package/templates/apps/api/e2e/api-service.spec.ts.hbs +2 -2
- package/templates/apps/api/hai.build.json.hbs +1 -1
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +6 -1
- package/templates/apps/api/playwright.config.ts.hbs +15 -7
- package/templates/apps/fullstack/.dockerignore.hbs +14 -9
- package/templates/apps/fullstack/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/src/App.svelte.hbs +7 -1
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/messages/ja-JP.json.hbs +12 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +24 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/messages/ja-JP.json.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +2 -2
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +30 -9
- package/templates/apps/fullstack/hai.build.json.hbs +2 -2
- package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
- package/templates/apps/fullstack/package.json.hbs +3 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/admin-schemas.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/index.ts.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/src/{{projectName}}-contract.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +32 -8
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +0 -6
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +8 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +2 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/package.json.hbs +6 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +9 -5
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs +3 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/messages.ts.hbs +16 -1
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +7 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs +3 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/runtime.ts.hbs +58 -7
- package/templates/apps/fullstack/packages/{{projectName}}-serv/tests/app-server.test.ts.hbs +13 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +40 -14
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/ja-JP.json.hbs +105 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +6 -6
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/audit.svelte.hbs +7 -7
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +4 -4
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/users.svelte.hbs +8 -8
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +13 -14
- package/templates/apps/fullstack/playwright.config.ts.hbs +15 -6
- package/templates/apps/h5/messages/ja-JP.json +40 -0
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +16 -5
- package/templates/apps/mobile-app/messages/ja-JP.json +36 -0
- package/templates/apps/mobile-app/src/App.svelte.hbs +15 -5
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +173 -14
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
- package/templates/apps/website/messages/ja-JP.json +32 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +13 -5
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
- package/templates/base/.dockerignore.hbs +14 -8
- package/templates/base/.gitignore.hbs +7 -0
- package/templates/base/e2e/app.spec.ts.hbs +23 -8
- package/templates/base/hai.build.json.hbs +1 -1
- package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
- package/templates/base/package.json.hbs +11 -3
- package/templates/base/playwright.config.ts +15 -4
- package/templates/base/src/hooks.server.ts.hbs +15 -6
- package/templates/base/src/lib/server/init.ts.hbs +139 -30
- package/templates/base/vite.config.ts.hbs +5 -3
- package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +11 -6
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +18 -25
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +4 -0
- package/templates/i18n/project.inlang/settings.json +1 -1
- package/templates/shared/.gitattributes +6 -0
- package/templates/shared/e2e-env.ts +98 -0
- package/templates/shared/run-e2e.mjs +51 -12
- package/templates/skills/AGENTS.md +7 -3
- package/templates/skills/bridges/admin/AGENTS.md +6 -2
- package/templates/skills/bridges/api/AGENTS.md +3 -1
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +5 -1
- package/templates/skills/bridges/h5/AGENTS.md +6 -2
- package/templates/skills/bridges/mobile-app/AGENTS.md +6 -2
- package/templates/skills/bridges/website/AGENTS.md +6 -2
- package/templates/skills/hai-ai/SKILL.md +8 -2
- package/templates/skills/hai-ai/guide.md +134 -60
- package/templates/skills/hai-ai/reference.md +51 -11
- package/templates/skills/hai-api-client/SKILL.md +15 -160
- package/templates/skills/hai-api-client/reference.md +178 -0
- package/templates/skills/hai-api-contract/SKILL.md +16 -273
- package/templates/skills/hai-api-contract/reference.md +280 -0
- package/templates/skills/hai-app-create/SKILL.md +6 -2
- package/templates/skills/hai-app-create/development.md +24 -8
- package/templates/skills/hai-app-create/reference.md +7 -5
- package/templates/skills/hai-app-review/SKILL.md +7 -3
- package/templates/skills/hai-app-review/differential.md +34 -0
- package/templates/skills/hai-app-tests/SKILL.md +6 -3
- package/templates/skills/hai-app-tests/reference.md +5 -5
- package/templates/skills/hai-app-tests/validation.md +38 -0
- package/templates/skills/hai-audit/SKILL.md +24 -147
- package/templates/skills/hai-audit/reference.md +148 -0
- package/templates/skills/hai-build/SKILL.md +14 -8
- package/templates/skills/hai-build/reference.md +16 -13
- package/templates/skills/hai-cache/SKILL.md +20 -181
- package/templates/skills/hai-cache/reference.md +229 -0
- package/templates/skills/hai-capacitor/SKILL.md +7 -4
- package/templates/skills/hai-capacitor/reference.md +15 -9
- package/templates/skills/hai-ci/SKILL.md +2 -0
- package/templates/skills/hai-core/SKILL.md +7 -3
- package/templates/skills/hai-core/reference.md +25 -5
- package/templates/skills/hai-crypto/SKILL.md +22 -250
- package/templates/skills/hai-crypto/reference.md +310 -0
- package/templates/skills/hai-datapipe/SKILL.md +16 -195
- package/templates/skills/hai-datapipe/reference.md +204 -0
- package/templates/skills/hai-deploy/SKILL.md +24 -54
- package/templates/skills/hai-deploy/middleware.md +51 -43
- package/templates/skills/hai-framework-sync/SKILL.md +2 -2
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-iam/SKILL.md +25 -11
- package/templates/skills/hai-iam/reference.md +181 -154
- package/templates/skills/hai-kit/SKILL.md +35 -174
- package/templates/skills/hai-kit/reference.md +190 -0
- package/templates/skills/hai-payment/SKILL.md +33 -263
- package/templates/skills/hai-payment/reference.md +295 -0
- package/templates/skills/hai-pr-review/SKILL.md +21 -88
- package/templates/skills/hai-reach/SKILL.md +6 -3
- package/templates/skills/hai-reach/reference.md +83 -22
- package/templates/skills/hai-reldb/SKILL.md +6 -3
- package/templates/skills/hai-reldb/reference.md +68 -25
- package/templates/skills/hai-scheduler/SKILL.md +18 -195
- package/templates/skills/hai-scheduler/reference.md +216 -0
- package/templates/skills/hai-serv/SKILL.md +25 -10
- package/templates/skills/hai-serv/reference.md +123 -87
- package/templates/skills/hai-spec-implement/SKILL.md +5 -1
- package/templates/skills/hai-spec-implement/evaluation.md +18 -0
- package/templates/skills/hai-spec-implement/handoff.md +47 -0
- package/templates/skills/hai-spec-implement/templates.md +5 -0
- package/templates/skills/hai-spec-implement/workflow.md +5 -3
- package/templates/skills/hai-spec-review/SKILL.md +4 -0
- package/templates/skills/hai-storage/SKILL.md +23 -238
- package/templates/skills/hai-storage/reference.md +282 -0
- package/templates/skills/hai-ui/SKILL.md +17 -3
- package/templates/skills/hai-ui/guide.md +3 -3
- package/templates/skills/hai-ui/reference.md +26 -11
- package/templates/skills/hai-ui-design/SKILL.md +31 -0
- package/templates/skills/hai-ui-design/patterns.md +26 -0
- package/templates/skills/hai-ui-review/SKILL.md +30 -0
- package/templates/skills/hai-ui-review/checklist.md +22 -0
- package/templates/skills/hai-vecdb/SKILL.md +14 -10
- package/templates/skills/hai-vecdb/reference.md +33 -3
- package/dist/chunk-FWN5B54L.js +0 -468
- package/dist/chunk-FWN5B54L.js.map +0 -1
- package/dist/chunk-QYX6V7H2.js.map +0 -1
- package/dist/cli-config-templates-3XSFIXBP.js +0 -5
- package/dist/src-RDQ2EIBT.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -35
- package/templates/apps/fullstack/Dockerfile.hbs +0 -57
- /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
- /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
|
@@ -12,7 +12,7 @@ import { defineConfig } from 'vitest/config'
|
|
|
12
12
|
const envDir = fileURLToPath(new URL('../../', import.meta.url))
|
|
13
13
|
|
|
14
14
|
export default defineConfig(({ mode, command }) => {
|
|
15
|
-
const envResult = command === 'build' ? { success: true as const, data: {} as Record<string, string> } : core.config.loadEnvFiles({ mode, envDir })
|
|
15
|
+
const envResult = command === 'build' || process.env.HAI_E2E === '1' ? { success: true as const, data: {} as Record<string, string> } : core.config.loadEnvFiles({ mode, envDir })
|
|
16
16
|
if (!envResult.success)
|
|
17
17
|
throw new Error(envResult.error.message)
|
|
18
18
|
const env = envResult.data
|
|
@@ -51,7 +51,7 @@ export default defineConfig(({ mode, command }) => {
|
|
|
51
51
|
},
|
|
52
52
|
envPrefix: ['VITE_', 'PUBLIC_'],
|
|
53
53
|
ssr: {
|
|
54
|
-
noExternal: [
|
|
54
|
+
noExternal: ['@h-ai/ui', /{{fullstack.sharedPackageName}}/],
|
|
55
55
|
},
|
|
56
56
|
test: {
|
|
57
57
|
environment: 'node',
|
|
@@ -13,7 +13,7 @@ const apiPrefix = '/api/v1'
|
|
|
13
13
|
const adminPassword = process.env.E2E_ADMIN_PASSWORD || 'Scaffold-test-Admin123!'
|
|
14
14
|
const configuredName = process.env.HAI_CORE_NAME || 'Configured scaffold'
|
|
15
15
|
const configuredVersion = process.env.HAI_CORE_VERSION || '9.8.7'
|
|
16
|
-
const configuredEnv =
|
|
16
|
+
const configuredEnv = 'test'
|
|
17
17
|
|
|
18
18
|
// 测试专用的一像素 PNG;上传仍使用完整前后端加密链路。
|
|
19
19
|
const avatar = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aSxkAAAAASUVORK5CYII=', 'base64')
|
|
@@ -61,7 +61,7 @@ test('service rejects plaintext business requests', async ({ request }) => {
|
|
|
61
61
|
})
|
|
62
62
|
|
|
63
63
|
{{#if fullstack.e2eFrontend}}
|
|
64
|
-
test('hash routing
|
|
64
|
+
test('hash routing and responsive workspace preserve usable content width', async ({ page }, testInfo) => {
|
|
65
65
|
await page.goto('/#/roles?source=direct')
|
|
66
66
|
await login(page, 'admin', adminPassword)
|
|
67
67
|
await expect(page.getByTestId('roles-page')).toBeVisible()
|
|
@@ -70,6 +70,16 @@ test('hash routing restores direct links, refresh and browser history', async ({
|
|
|
70
70
|
await expect(page.getByTestId('roles-page')).toBeVisible()
|
|
71
71
|
await expect(page.locator('header')).toBeVisible()
|
|
72
72
|
await expect(page.locator('footer')).toBeVisible()
|
|
73
|
+
for (const width of [1440, 1920, 2560, 3840, 390]) {
|
|
74
|
+
await page.setViewportSize({ width, height: 960 })
|
|
75
|
+
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
|
|
76
|
+
const main = await page.locator('main').boundingBox()
|
|
77
|
+
const content = await page.getByTestId('roles-page').boundingBox()
|
|
78
|
+
expect(content!.width).toBeGreaterThan(main!.width - 80)
|
|
79
|
+
if (width === 1920)
|
|
80
|
+
await capture(page, '{{projectName}}-wide.png', testInfo.outputPath('wide-1920.png'))
|
|
81
|
+
}
|
|
82
|
+
await page.setViewportSize({ width: 1440, height: 960 })
|
|
73
83
|
await page.getByTestId('nav-settings').click()
|
|
74
84
|
await expect(page).toHaveURL(/#\/settings$/)
|
|
75
85
|
await page.goBack()
|
|
@@ -251,14 +261,15 @@ test('shared authentication supports public registration with configuration', as
|
|
|
251
261
|
await expect(page.getByTestId('login-form')).toBeVisible()
|
|
252
262
|
await page.screenshot({ path: path.join(evidence, '{{projectName}}-login.png'), fullPage: true })
|
|
253
263
|
await page.getByRole('button', { name: /立即注册|Register now/i }).click()
|
|
254
|
-
|
|
264
|
+
const registerForm = page.locator('.register-form')
|
|
265
|
+
await expect(registerForm.locator('#register-username')).toBeVisible()
|
|
255
266
|
await page.screenshot({ path: path.join(evidence, '{{projectName}}-register.png'), fullPage: true })
|
|
256
|
-
const username = `signup${
|
|
257
|
-
await
|
|
258
|
-
await
|
|
259
|
-
await
|
|
260
|
-
await
|
|
261
|
-
await
|
|
267
|
+
const username = `signup${randomUUID().replaceAll('-', '').slice(0, 12)}`
|
|
268
|
+
await registerForm.locator('#register-username').fill(username)
|
|
269
|
+
await registerForm.locator('#register-email').fill(`${username}@example.com`)
|
|
270
|
+
await registerForm.locator('#register-password').fill('SignupPassword123')
|
|
271
|
+
await registerForm.locator('#register-confirm-password').fill('SignupPassword123')
|
|
272
|
+
await registerForm.locator('button[type="submit"]').click()
|
|
262
273
|
await expect(page.getByTestId('settings-page')).toBeVisible()
|
|
263
274
|
expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
|
|
264
275
|
if (!process.env.IAM_NOTIFICATION_URL)
|
|
@@ -448,6 +459,16 @@ test('administrator and user complete account, avatar, preference, password and
|
|
|
448
459
|
await expect(userPage.getByTestId('profile-form').locator('input[name="displayName"]')).toHaveValue('Updated member')
|
|
449
460
|
await expect(userPage.getByTestId('profile-form').locator('input[name="email"]')).toHaveValue('')
|
|
450
461
|
await expect.poll(() => userPage.getByTestId('profile-form').locator('img').evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true)
|
|
462
|
+
await userPage.getByTestId('profile-language').click()
|
|
463
|
+
await userPage.getByRole('option', { name: '日本語', exact: true }).click()
|
|
464
|
+
await expect(userPage.getByTestId('nav-settings')).toHaveText('個人設定')
|
|
465
|
+
await expect(userPage.locator('html')).toHaveAttribute('lang', 'ja-JP')
|
|
466
|
+
await userPage.reload()
|
|
467
|
+
await expect(userPage.getByTestId('nav-settings')).toHaveText('個人設定')
|
|
468
|
+
await expect(userPage.locator('html')).toHaveAttribute('lang', 'ja-JP')
|
|
469
|
+
await userPage.getByTestId('profile-language').click()
|
|
470
|
+
await userPage.getByRole('option', { name: 'English', exact: true }).click()
|
|
471
|
+
await expect(userPage.getByTestId('nav-settings')).toHaveText('Personal settings')
|
|
451
472
|
await userPage.setViewportSize({ width: 390, height: 844 })
|
|
452
473
|
await expect.poll(() => userPage.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth)).toBe(true)
|
|
453
474
|
await userPage.getByTestId('mobile-menu').click()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"images": [
|
|
3
|
-
{ "name": "app", "
|
|
4
|
-
{ "name": "web", "
|
|
3
|
+
{ "name": "app", "dockerfile": "images/app.Dockerfile", "containerPort": 3000 }{{#if fullstack.frontends.web}},
|
|
4
|
+
{ "name": "web", "dockerfile": "images/web.Dockerfile", "containerPort": 8080 }{{/if}}
|
|
5
5
|
]{{#if fullstack.frontends.desktop}},
|
|
6
6
|
"desktop": ["apps/{{projectName}}-desktop"]{{/if}}
|
|
7
7
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
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
|
|
4
|
+
|
|
5
|
+
ENV PNPM_HOME=/pnpm
|
|
6
|
+
ENV PATH=$PNPM_HOME:$PATH
|
|
7
|
+
ENV CI=true
|
|
8
|
+
ENV ONNXRUNTIME_NODE_INSTALL_CUDA=skip
|
|
9
|
+
WORKDIR /workspace
|
|
10
|
+
|
|
11
|
+
RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
|
|
12
|
+
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
13
|
+
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 pnpm --filter {{fullstack.servPackageName}}... --if-present build
|
|
18
|
+
|
|
19
|
+
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
|
|
20
|
+
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
|
|
21
|
+
|
|
22
|
+
FROM node:24.22.1-bookworm-slim AS service
|
|
23
|
+
|
|
24
|
+
ENV NODE_ENV=production
|
|
25
|
+
ENV HOST=0.0.0.0
|
|
26
|
+
ENV PORT=3000
|
|
27
|
+
WORKDIR /app
|
|
28
|
+
|
|
29
|
+
COPY --from=build --chown=node:node /prod/service ./
|
|
30
|
+
RUN mkdir -p /app/data && chown node:node /app/data
|
|
31
|
+
|
|
32
|
+
USER node
|
|
33
|
+
EXPOSE 3000
|
|
34
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
|
35
|
+
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))"
|
|
36
|
+
CMD ["node", "dist/index.js"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{{#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
|
|
5
|
+
|
|
6
|
+
ENV PNPM_HOME=/pnpm
|
|
7
|
+
ENV PATH=$PNPM_HOME:$PATH
|
|
8
|
+
ENV CI=true
|
|
9
|
+
WORKDIR /workspace
|
|
10
|
+
|
|
11
|
+
RUN corepack enable && corepack prepare pnpm@12.11.2 --activate && pnpm config set store-dir /pnpm/store
|
|
12
|
+
COPY pnpm-lock.yaml pnpm-workspace.yaml ./
|
|
13
|
+
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 pnpm --filter {{fullstack.sharedPackageName}} paraglide:compile && pnpm --filter {{projectName}}-web... --if-present build
|
|
18
|
+
|
|
19
|
+
FROM nginxinc/nginx-unprivileged:1.29.8-alpine AS web
|
|
20
|
+
|
|
21
|
+
ENV PUBLIC_API_BASE=
|
|
22
|
+
ENV NGINX_ENVSUBST_FILTER=PUBLIC_API_BASE_BASE64
|
|
23
|
+
ENV NGINX_ENVSUBST_OUTPUT_DIR=/tmp/hai-runtime-config
|
|
24
|
+
|
|
25
|
+
RUN mkdir -p /tmp/hai-runtime-config
|
|
26
|
+
COPY images/web/nginx.conf /etc/nginx/conf.d/default.conf
|
|
27
|
+
COPY --chmod=755 images/web/19-runtime-config.envsh /docker-entrypoint.d/19-runtime-config.envsh
|
|
28
|
+
COPY images/web/runtime-config.js.template /etc/nginx/templates/runtime-config.js.template
|
|
29
|
+
COPY --from=build /workspace/apps/{{projectName}}-web/dist /usr/share/nginx/html
|
|
30
|
+
|
|
31
|
+
EXPOSE 8080
|
|
32
|
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
33
|
+
CMD wget -Y off -q -O /dev/null http://127.0.0.1:8080/health || exit 1
|
|
34
|
+
{{/if}}
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
"start:prod": "pnpm --filter {{fullstack.servPackageName}} start:prod",
|
|
16
16
|
"dev:test": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev:test",
|
|
17
17
|
"dev:prod": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm --parallel --filter {{fullstack.servPackageName}}{{#each fullstack.frontendApps}} --filter {{packageName}}{{/each}} dev:prod",
|
|
18
|
-
"compile": "pnpm
|
|
18
|
+
"compile": "pnpm i18n:compile && pnpm -r --if-present build",
|
|
19
19
|
"build": "hai build",
|
|
20
|
+
{{#if features.deploy}}
|
|
20
21
|
"deploy": "hai deploy packages/{{projectName}}-serv",
|
|
22
|
+
{{/if}}
|
|
21
23
|
"check": "pnpm typecheck",
|
|
22
24
|
"typecheck": "pnpm --filter {{fullstack.contractPackageName}} build && pnpm i18n:compile && pnpm -r --if-present typecheck",
|
|
23
25
|
"lint": "eslint *.js *.ts e2e && pnpm -r --if-present lint",
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# {{fullstack.contractPackageName}}
|
|
2
2
|
|
|
3
|
+
## 简介
|
|
4
|
+
|
|
3
5
|
共享 API 契约包,包含 Zod Schema、`apiContract.route()` 业务接口定义与工程级 contract。业务 schema、角色、流程及限额是模板初始方案,按用户需求修改,并同步服务端授权、调用方、测试与说明;不要求需求沿用默认业务。
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## 核心功能清单
|
|
8
|
+
|
|
9
|
+
- 共享接口路径、输入输出 Schema 和类型。
|
|
10
|
+
- 客户端与服务端使用同一份 contract。
|
|
11
|
+
- 不包含服务端业务实现,也不加载私有配置。
|
|
12
|
+
|
|
13
|
+
## 使用入门
|
|
14
|
+
|
|
15
|
+
### 骨架说明
|
|
6
16
|
|
|
7
17
|
- `src/app-schemas.ts`:跨接口、跨层复用的输入/数据 Schema 与类型。
|
|
8
18
|
- `src/app-contract.ts`:app 领域 contract;每个业务 path 直接写在对应接口定义中。
|
|
@@ -13,7 +23,18 @@
|
|
|
13
23
|
API 前缀、健康检查与跨端 Header 等传输配置位于 serv 包的 `config/_serv.yml`。
|
|
14
24
|
`SUPPORTED_LOCALES` 为生成时实际编译的语言列表;`app.info` 中名称、版本、环境、默认语言来自后端 core 配置。更新公开契约时同时更新 serv、shared typed API client、各端 UI 与测试。
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
### 开发
|
|
17
27
|
|
|
18
28
|
- `pnpm --filter {{fullstack.contractPackageName}} typecheck`
|
|
19
29
|
- `pnpm --filter {{fullstack.contractPackageName}} test`
|
|
30
|
+
|
|
31
|
+
## 开发测试
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pnpm --filter {{fullstack.contractPackageName}} typecheck
|
|
35
|
+
pnpm --filter {{fullstack.contractPackageName}} test
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 许可证
|
|
39
|
+
|
|
40
|
+
Apache-2.0
|
|
@@ -76,7 +76,7 @@ export const AdminAuditSchema = z.object({
|
|
|
76
76
|
export const ProfileUpdateSchema = z.object({
|
|
77
77
|
displayName: z.string().max(100).optional(),
|
|
78
78
|
email: z.union([z.email(), z.literal('')]).optional(),
|
|
79
|
-
locale: z.enum(['
|
|
79
|
+
locale: z.enum([{{#each fullstack.i18n.locales}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}]).optional(),
|
|
80
80
|
theme: z.string().min(1).max(40).regex(/^[\w-]+$/).optional(),
|
|
81
81
|
}).strict()
|
|
82
82
|
/** 512 KiB 头像以 JSON 传输,上传及响应均经过统一加密。 */
|
|
@@ -2,12 +2,15 @@ import { apiContract } from '@h-ai/api-contract'
|
|
|
2
2
|
import { adminContract, authContract, profileContract } from './admin-contract.js'
|
|
3
3
|
import { appContract } from './app-contract.js'
|
|
4
4
|
|
|
5
|
+
// @hai-generator:contracts:imports
|
|
6
|
+
|
|
5
7
|
/** 当前工程启用的前端目标。 */
|
|
6
8
|
export const ENABLED_FRONTENDS = {{{fullstack.selectedFrontendsJson}}} as const
|
|
7
9
|
export const SUPPORTED_LOCALES = [{{#each fullstack.i18n.locales}}'{{this}}'{{#unless @last}}, {{/unless}}{{/each}}] as const
|
|
8
10
|
|
|
9
11
|
/** 工程级 API contract,前端 typed client 与后端 service 共用。 */
|
|
10
12
|
export const {{fullstack.contractExportName}} = apiContract.create({
|
|
13
|
+
// @hai-generator:contracts:entries
|
|
11
14
|
app: appContract,
|
|
12
15
|
iam: { auth: authContract },
|
|
13
16
|
admin: adminContract,
|
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# {{fullstack.servPackageName}}
|
|
2
2
|
|
|
3
|
+
## 简介
|
|
4
|
+
|
|
3
5
|
后端 API Service,基于 `@h-ai/serv` 将共享 contract 装配为最小 HTTP App 抽象,不在应用代码中直接暴露 Hono。
|
|
4
6
|
|
|
5
|
-
##
|
|
7
|
+
## 核心功能清单
|
|
8
|
+
|
|
9
|
+
- HTTP 服务、健康检查和共享 contract 的 procedure 实现。
|
|
10
|
+
- 用户、角色、审计与个人设置的示例业务。
|
|
11
|
+
- 模块配置、加密传输与服务端权限校验。
|
|
12
|
+
|
|
13
|
+
## 使用入门
|
|
14
|
+
|
|
15
|
+
### 默认功能与配置
|
|
6
16
|
|
|
7
17
|
以下是可运行示例的初始实现,角色/权限、管理流程、头像限额、分页、审计策略及部署方案均可按用户需求调整。需求与代码或模板规则不一致时,以需求为业务基线同步实现、测试与过期说明;实际框架 API 契约和服务端安全校验仍需满足。
|
|
8
18
|
|
|
@@ -11,6 +21,7 @@
|
|
|
11
21
|
- `config/_db.yml` / `_cache.yml` / `_iam.yml` / `_storage.yml` / `_audit.yml` / `_reach.yml`:管理及开发通知配置,初始化前使用模块公开 Schema 验证。Reach 默认控制台 Provider,生产按注释替换为 SMTP/短信/API。
|
|
12
22
|
- `config/_ai.yml` / `_vecdb.yml` / `_payment.yml` / `_scheduler.yml` / `_deploy.yml`:保留未启用模块的完整注释示例;配置文件存在不代表模块已接入。先在本 workspace 安装对应依赖,再在 `src/server/runtime.ts` 按公开 Schema 校验、初始化并反序关闭,接入实际业务后按需取消注释。整份注释先去掉一层外层注释,可选字段继续按需开启。
|
|
13
23
|
- `src/index.ts`:进程入口,先 `core.init({ configDir: './config' })`,再初始化 reldb → cache → crypto → storage → reach(有配置时)→ iam → audit;启动失败及退出反序释放。
|
|
24
|
+
- 选择 AI 时自动安装 reldb/vecdb/AI,并在 reldb/cache 之后初始化 vecdb、在其余依赖之后初始化 AI;关闭时先释放 AI,最后释放 vecdb/关系库,关闭失败会返回错误并保留重试义务。生产环境必须使用共享后端,根 `.env.prod` 默认连接 Compose `db/cache/storage/vecdb`,凭据和真实 Reach Provider JSON 私有注入。
|
|
14
25
|
- `src/app-server.ts`:服务装配入口,挂载健康检查、OpenAPI、docs 与 procedures;应用名称 / 版本与 CORS 来源均取自上述配置。
|
|
15
26
|
- `src/server/procedures/app-procedures.ts`:app 领域 procedures,对应 contract 中的 `app.info` / `app.echo` 路由。
|
|
16
27
|
- `src/server/procedures/admin-procedures.ts`:认证、管理员用户/角色/审计管理和个人设置,使用 `.auth()` / `.role('admin')` 守卫。每个请求读取用户最新启用状态、角色和权限。
|
|
@@ -20,33 +31,46 @@
|
|
|
20
31
|
|
|
21
32
|
CLI 已生成根目录 `.env.local`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项目独立的随机强密码;`dev`/`start` 自动读取,首次登录使用用户名 `admin` 和该密码。仅在没有用户时创建管理员,不重设已有用户密码。密码不符合 `_iam.yml` 策略时,启动日志显示具体原因及错误码。`.env.local` 被 Git 和容器构建忽略,`.env.example` 不含真实密码;部署时设置目标环境自己的密码。所有业务 JSON 请求和响应强制加密;健康检查和随机路径的只读头像图片可直接访问。头像支持 PNG/JPEG/WebP,最大 512 KiB,校验内容特征和大小后由服务端生成 key,不能指定任意文件路径。
|
|
22
33
|
|
|
23
|
-
默认 SQLite + 本地 storage + memory cache 适合单实例,管理写入仅支持单写实例:进程内保护无过期时间,共享 cache 锁定期续租,丢失锁或释放失败会记录错误。多实例读取可使用共享数据库、Redis cache 和 S3 storage,但管理写请求必须统一路由到同一写实例;仅更换 Redis
|
|
34
|
+
默认 SQLite + 本地 storage + memory cache 适合单实例,管理写入仅支持单写实例:进程内保护无过期时间,共享 cache 锁定期续租,丢失锁或释放失败会记录错误。多实例读取可使用共享数据库、Redis cache 和 S3 storage,但管理写请求必须统一路由到同一写实例;仅更换 Redis 不提供任意多写副本的最后管理员原子保证。传输在所有节点共享受管版本化密钥环与 registry,不依赖会话粘性;重启加载原 ring,轮换先分发新旧版本再切 activeKeyId。Web 使用 HttpOnly refresh Cookie + 内存 access token;仅同时满足客户端标识、body 通道头与受信原生 Origin 的 App/Desktop 请求返回 refresh token body。
|
|
24
35
|
|
|
25
36
|
业务修改提交后的审计记录采用最大努力策略:写入失败输出包含操作及资源的结构化错误日志,业务仍返回原操作结果,避免将已经完成的变更误报为失败;应用启动时 audit 初始化失败则停止启动。
|
|
26
37
|
|
|
27
|
-
|
|
38
|
+
### 开发
|
|
28
39
|
|
|
29
40
|
所有配置字段以模块 Schema 为准;未使用字段和替代 Provider 用注释保留。叶子环境变量只覆盖已有 YAML 路径,先取消相应键的注释,或用 `HAI_DB` / `HAI_CACHE` 等整体对象覆盖;`connectTimeout` 对应 `HAI_CACHE_CONNECTTIMEOUT`。凭据示例在根 `.env.example`,实际密钥保存在忽略的 `.env.local` 或部署环境。`.gitignore` 的 `**/data/` 忽略所有运行数据及 `.gitkeep`;运行时会创建目录。`crypto.init()` 无配置参数,datapipe 为纯函数,不提供虚构配置。
|
|
30
41
|
|
|
42
|
+
CLI 同时在根 `.env.local` 一次性生成独立开发用 `HAI_CRYPTO_TRANSPORT_SERVERKEYRING`,结构为 `{activeKeyId,keys:{id:{publicKey,privateKey}}}`,运行时不隐式生成。部署由 Secret Manager 向所有节点分发相同受管 ring,客户端 registry 复用共享 reldb。私钥不进入普通 YAML、PUBLIC_/VITE_、日志或公开响应。轮换先分发含新旧版本的 ring,再切 activeKeyId 并滚动重建;过渡期后删除旧 ID,旧客户端 destroy 后重协商。没有 sticky session 依赖。
|
|
43
|
+
|
|
31
44
|
自动生成的 `.env` 包含已启用模块的环境参数;YAML 显式引用相同变量并保留本地回退。使用 `start:test` / `start:prod` 加载对应环境覆盖。
|
|
32
45
|
|
|
33
46
|
- `pnpm --filter {{fullstack.servPackageName}} dev`
|
|
34
47
|
- `pnpm --filter {{fullstack.servPackageName}} test`
|
|
35
48
|
|
|
36
|
-
|
|
49
|
+
### 容器运行
|
|
37
50
|
|
|
38
|
-
从工程根运行 `pnpm build --target docker`
|
|
51
|
+
从工程根运行 `pnpm build --target docker` 导出镜像。默认带部署工具,可运行 `pnpm run deploy --env test --build` 一次完成构建发布,或 `pnpm run deploy --env prod --bundle-only` 交付已有镜像及跨平台脚本。完整包包含自建中间件,人工部署与按服务更新见工程根 README。
|
|
39
52
|
|
|
40
53
|
- 服务镜像直接由 `@h-ai/serv` 对外监听,配置挂载点为 `/app/config`,数据目录为 `/app/data`,不包含额外转发进程。
|
|
41
54
|
- Web 镜像不烘焙 API 地址;启动时使用 `-e PUBLIC_API_BASE=https://api.example.com` 注入,同一镜像可部署到不同环境。未设置时 Web 默认请求自身 origin。
|
|
42
55
|
- YAML 配置使用 `${ENV_NAME:default}` 接收运行时环境变量;不要把密钥写入镜像。
|
|
43
56
|
|
|
44
|
-
|
|
57
|
+
### IAM 认证配置
|
|
45
58
|
|
|
46
|
-
服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token
|
|
59
|
+
服务端 `config/_iam.yml` 统一控制登录、注册、重置、协议与 `ui` 品牌。邮件/短信可用 `_reach.yml` 中的 Provider/标准模板或自定义发送回调。`AuthPanel` 使用安全公开配置及框架认证动作;无需在页面适配 Token、验证码倒计时和错误提示。单实例可使用 Memory;多实例需共享数据库与 Redis,并按工程根 README 配置写入路由和传输会话。
|
|
47
60
|
|
|
48
|
-
LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise
|
|
61
|
+
LDAP/Reach 均可选:基础认证无需安装。默认 LDAP 客户端需在 IAM 服务端 workspace 安装 ldapts,再配置 login.ldap 与连接信息。自定义 onOtpSendEmail/onOtpSendSms/onPasswordResetRequest 回调可独立于 Reach 使用,投递失败须拒绝 Promise;回调第三参数 context 提供 expiresAt/idempotencyKey,发送服务按稳定键持久化去重,异常/超时为结果未知且不自动重发。协议在 Modal 显示;LoginForm/RegisterForm 提供 header/extraFields/footer,AuthPanel 透传 loginHeader/loginFields/loginFooter 和 registerHeader/registerFields/registerFooter。配置及例子见生成项目的 hai-iam/hai-ui skills。
|
|
49
62
|
|
|
50
63
|
协议支持配置中的 userAgreementMarkdown/privacyPolicyMarkdown 正文及 URL;同主题优先 Markdown,由安全渲染器在弹窗显示。
|
|
51
64
|
|
|
52
65
|
完整契约见生成应用的 `.agents/skills/hai-iam/reference.md`。
|
|
66
|
+
|
|
67
|
+
## 开发测试
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
pnpm --filter {{fullstack.servPackageName}} typecheck
|
|
71
|
+
pnpm --filter {{fullstack.servPackageName}} test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## 许可证
|
|
75
|
+
|
|
76
|
+
Apache-2.0
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
{{yaml 'name' fullstack.appName}}
|
|
10
10
|
version: 0.1.0
|
|
11
11
|
env: development
|
|
12
|
-
debug: false
|
|
13
12
|
|
|
14
13
|
defaultLocale: {{fullstack.i18n.baseLocale}}
|
|
15
14
|
logging:
|
|
@@ -21,8 +20,3 @@ logging:
|
|
|
21
20
|
- password
|
|
22
21
|
- token
|
|
23
22
|
- secret
|
|
24
|
-
|
|
25
|
-
# 短 ID 生成配置;未设置时采用框架默认值。
|
|
26
|
-
# id:
|
|
27
|
-
# prefix: app_
|
|
28
|
-
# length: 21
|
|
@@ -28,6 +28,7 @@ providers:
|
|
|
28
28
|
# headers:
|
|
29
29
|
# Authorization: '${HAI_REACH_PROVIDERS_0_HEADERS_AUTHORIZATION}'
|
|
30
30
|
# timeout: 10000
|
|
31
|
+
# idempotent: ${HAI_REACH_PROVIDERS_0_IDEMPOTENT:false} # 仅接收端具备原子持久去重时启用;默认在途不明结果转 unknown
|
|
31
32
|
templates:
|
|
32
33
|
- name: otp_email
|
|
33
34
|
provider: email
|
|
@@ -50,3 +51,10 @@ templates:
|
|
|
50
51
|
# strategy: discard # discard | delay
|
|
51
52
|
# start: '22:00'
|
|
52
53
|
# end: '08:00'
|
|
54
|
+
|
|
55
|
+
# 延迟队列策略随每条消息保存;最多领取 5 次,指数退避 1 分钟至 1 小时。
|
|
56
|
+
# 超限 dead 只经可信服务端人工恢复;unknown 不允许自动重发。
|
|
57
|
+
queue:
|
|
58
|
+
maxAttempts: ${HAI_REACH_QUEUE_MAXATTEMPTS:5}
|
|
59
|
+
retryDelayMs: ${HAI_REACH_QUEUE_RETRYDELAYMS:60000}
|
|
60
|
+
maxRetryDelayMs: ${HAI_REACH_QUEUE_MAXRETRYDELAYMS:3600000}
|
|
@@ -42,11 +42,12 @@ cors:
|
|
|
42
42
|
- X-HAI-Token-Transport
|
|
43
43
|
- X-Client-Id
|
|
44
44
|
- X-Encrypted
|
|
45
|
+
- X-Hai-Transport
|
|
45
46
|
exposedHeaders:
|
|
46
47
|
- X-Request-Id
|
|
47
48
|
- X-Encrypted
|
|
49
|
+
- X-Hai-Transport
|
|
48
50
|
|
|
49
51
|
transport:
|
|
50
52
|
keyExchangePath: /_hai/key-exchange
|
|
51
|
-
maxClients: 10000
|
|
52
53
|
# excludePaths: [/health, /ready]
|
|
@@ -35,6 +35,9 @@
|
|
|
35
35
|
"start:prod": "pnpm start"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
{{#if features.ai}}
|
|
39
|
+
"@h-ai/ai": "{{haiPackageSpecifier}}",
|
|
40
|
+
{{/if}}
|
|
38
41
|
"@h-ai/audit": "{{haiPackageSpecifier}}",
|
|
39
42
|
"@h-ai/cache": "{{haiPackageSpecifier}}",
|
|
40
43
|
"@h-ai/core": "{{haiPackageSpecifier}}",
|
|
@@ -44,6 +47,9 @@
|
|
|
44
47
|
"@h-ai/reldb": "{{haiPackageSpecifier}}",
|
|
45
48
|
"@h-ai/serv": "{{haiPackageSpecifier}}",
|
|
46
49
|
"@h-ai/storage": "{{haiPackageSpecifier}}",
|
|
50
|
+
{{#if features.vecdb}}
|
|
51
|
+
"@h-ai/vecdb": "{{haiPackageSpecifier}}",
|
|
52
|
+
{{/if}}
|
|
47
53
|
"zod": "{{fullstack.packageSpecifiers.zod}}",
|
|
48
54
|
"{{fullstack.contractPackageName}}": "workspace:*"
|
|
49
55
|
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { CoreConfig } from '@h-ai/core'
|
|
2
2
|
import type { ServHttpApp } from '@h-ai/serv'
|
|
3
|
+
import process from 'node:process'
|
|
3
4
|
import { core } from '@h-ai/core'
|
|
4
|
-
import { createReldbTransportKeyStore, crypto } from '@h-ai/crypto'
|
|
5
|
+
import { createReldbTransportKeyStore, crypto, parseTransportServerKeyRing } from '@h-ai/crypto'
|
|
5
6
|
import { iam } from '@h-ai/iam'
|
|
6
7
|
import { reldb } from '@h-ai/reldb'
|
|
7
8
|
import { serv, ServConfigSchema } from '@h-ai/serv'
|
|
@@ -13,6 +14,9 @@ const STARTED_AT = new Date().toISOString()
|
|
|
13
14
|
|
|
14
15
|
/** 创建后端 HTTP App;与 @h-ai/serv 的 createApp 返回语义保持一致。 */
|
|
15
16
|
export function createServerApp(): ServHttpApp {
|
|
17
|
+
const ring = parseTransportServerKeyRing(process.env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING)
|
|
18
|
+
if (!ring.success)
|
|
19
|
+
throw new Error(ring.error.message)
|
|
16
20
|
const coreConfig = core.config.getOrThrow<CoreConfig>('core')
|
|
17
21
|
const servConfig = ServConfigSchema.parse(core.config.getOrThrow('serv'))
|
|
18
22
|
const security = serv.createRuntimeSecurityPolicy({
|
|
@@ -46,8 +50,8 @@ export function createServerApp(): ServHttpApp {
|
|
|
46
50
|
&& ['{{projectName}}-app', '{{projectName}}-desktop'].includes(request.headers.get('x-request-client') ?? '')
|
|
47
51
|
&& security.isNativeOrigin(request.headers.get('origin') ?? '') },
|
|
48
52
|
},
|
|
49
|
-
//
|
|
50
|
-
transport: { ...(servConfig.transport || {}), excludePaths: publicPaths, crypto, keyStore: createReldbTransportKeyStore({ reldb }) },
|
|
53
|
+
// 管理业务默认强制双向加密;各节点复用受管服务端密钥环与共享 reldb 客户端 registry。
|
|
54
|
+
transport: { ...(servConfig.transport || {}), excludePaths: publicPaths, crypto, serverKeyRing: ring.data, keyStore: createReldbTransportKeyStore({ reldb, ttlSeconds: 3600 }) },
|
|
51
55
|
http: {
|
|
52
56
|
...servConfig.http,
|
|
53
57
|
openapi: security.exposeApiDocs ? servConfig.http.openapi : false,
|
|
@@ -57,8 +61,8 @@ export function createServerApp(): ServHttpApp {
|
|
|
57
61
|
middleware: serv.cors({
|
|
58
62
|
origin: security.allowOrigin,
|
|
59
63
|
credentials: servConfig.cors.credentials,
|
|
60
|
-
allowedHeaders: [...new Set([...servConfig.cors.allowedHeaders, 'X-Client-Id', 'X-Encrypted', 'X-Request-Client', 'X-HAI-Token-Transport'])],
|
|
61
|
-
exposedHeaders: [...new Set([...servConfig.cors.exposedHeaders, 'X-Encrypted'])],
|
|
64
|
+
allowedHeaders: [...new Set([...servConfig.cors.allowedHeaders, 'X-Client-Id', 'X-Encrypted', 'X-Hai-Transport', 'X-Request-Client', 'X-HAI-Token-Transport'])],
|
|
65
|
+
exposedHeaders: [...new Set([...servConfig.cors.exposedHeaders, 'X-Encrypted', 'X-Hai-Transport'])],
|
|
62
66
|
}),
|
|
63
67
|
}, {
|
|
64
68
|
path: `${servConfig.http.apiPrefix}/assets/*`,
|
package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/admin-service.ts.hbs
CHANGED
|
@@ -21,18 +21,19 @@ export async function withAdminMutation<T>(context: ServContext, work: () => Pro
|
|
|
21
21
|
if (adminMutationActive)
|
|
22
22
|
return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
|
|
23
23
|
adminMutationActive = true
|
|
24
|
-
|
|
24
|
+
let owner = ''
|
|
25
25
|
const key = 'hai:app:admin-mutation'
|
|
26
26
|
let acquired = false
|
|
27
27
|
let leaseLost = false
|
|
28
28
|
let renewalPending = false
|
|
29
29
|
let renewalTimer: ReturnType<typeof setInterval> | undefined
|
|
30
30
|
try {
|
|
31
|
-
const locked = await cache.lock.acquire(key, {
|
|
31
|
+
const locked = await cache.lock.acquire(key, { ttl: 300 })
|
|
32
32
|
if (!locked.success)
|
|
33
33
|
return locked
|
|
34
34
|
if (!locked.data)
|
|
35
35
|
return err(HaiCommonError.CONFLICT, serverM('busy', { locale: context.locale }))
|
|
36
|
+
owner = locked.data
|
|
36
37
|
acquired = true
|
|
37
38
|
renewalTimer = setInterval(() => {
|
|
38
39
|
if (renewalPending || leaseLost)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { core } from '@h-ai/core'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** 服务端业务提示与角色种子名称同步维护中英日文。 */
|
|
4
4
|
export const serverM = core.i18n.createMessageGetter({
|
|
5
5
|
'zh-CN': {
|
|
6
6
|
admin: '管理员',
|
|
@@ -8,18 +8,33 @@ export const serverM = core.i18n.createMessageGetter({
|
|
|
8
8
|
adminDescription: '管理用户、角色和审计日志',
|
|
9
9
|
userDescription: '仅可维护个人设置',
|
|
10
10
|
notFound: '用户或角色不存在',
|
|
11
|
+
unauthorized: '请提供要撤销的访问令牌',
|
|
11
12
|
selfProtection: '不能删除、禁用或降级当前管理员账号',
|
|
12
13
|
invalidAvatar: '请选择不超过 512 KB 的有效 PNG、JPEG 或 WebP 图片',
|
|
13
14
|
busy: '系统繁忙,请稍后重试',
|
|
14
15
|
unsupportedPreference: '不支持此语言或主题',
|
|
15
16
|
invalidDateRange: '结束时间不能早于开始时间',
|
|
16
17
|
},
|
|
18
|
+
'ja-JP': {
|
|
19
|
+
admin: '管理者',
|
|
20
|
+
user: 'ユーザー',
|
|
21
|
+
adminDescription: 'ユーザー、ロール、監査ログを管理',
|
|
22
|
+
userDescription: '個人設定のみ管理',
|
|
23
|
+
notFound: 'ユーザーまたはロールが存在しません',
|
|
24
|
+
unauthorized: '取り消すアクセストークンを指定してください',
|
|
25
|
+
selfProtection: '自分の管理者アカウントは削除、無効化、降格できません',
|
|
26
|
+
invalidAvatar: '512KB以下の有効なPNG、JPEG、WebP画像を選択してください',
|
|
27
|
+
busy: 'システムが混雑しています。再試行してください',
|
|
28
|
+
unsupportedPreference: 'この言語またはテーマには対応していません',
|
|
29
|
+
invalidDateRange: '終了日は開始日以降にしてください',
|
|
30
|
+
},
|
|
17
31
|
'en-US': {
|
|
18
32
|
admin: 'Administrator',
|
|
19
33
|
user: 'User',
|
|
20
34
|
adminDescription: 'Manage users, roles and audit logs',
|
|
21
35
|
userDescription: 'Manage personal settings only',
|
|
22
36
|
notFound: 'User or role does not exist',
|
|
37
|
+
unauthorized: 'Provide the access token to revoke',
|
|
23
38
|
selfProtection: 'You cannot delete, disable or demote your own administrator account',
|
|
24
39
|
invalidAvatar: 'Choose a valid PNG, JPEG or WebP image up to 512 KB',
|
|
25
40
|
busy: 'The system is busy. Please try again',
|
|
@@ -29,11 +29,15 @@ export function createAuthProcedures() {
|
|
|
29
29
|
return result
|
|
30
30
|
await recordAudit({ ...context, session: { userId: result.data.user.id, roles: result.data.roles, permissions: result.data.permissions } }, 'login', 'auth')
|
|
31
31
|
return result
|
|
32
|
-
}).route('logout'
|
|
33
|
-
const
|
|
32
|
+
}).route('logout', async ({ input, context }) => {
|
|
33
|
+
const token = input.accessToken ?? context.accessToken
|
|
34
|
+
if (!token)
|
|
35
|
+
return err(HaiCommonError.UNAUTHORIZED, serverM('unauthorized', { locale: context.locale }))
|
|
36
|
+
const result = await iam.auth.logout(token)
|
|
34
37
|
if (!result.success)
|
|
35
38
|
return result
|
|
36
|
-
|
|
39
|
+
if (context.session)
|
|
40
|
+
await recordAudit(context, 'logout', 'auth')
|
|
37
41
|
return result
|
|
38
42
|
}).route('changePassword').auth().handle(async ({ input, context }) => {
|
|
39
43
|
const result = await iam.user.changeCurrentUserPassword(context.accessToken ?? '', input.oldPassword, input.newPassword)
|
package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/index.ts.hbs
CHANGED
|
@@ -2,6 +2,8 @@ import type { CreateAppProceduresOptions } from './app-procedures.js'
|
|
|
2
2
|
import { createAdminProcedures, createAuthProcedures, createProfileProcedures } from './admin-procedures.js'
|
|
3
3
|
import { createAppProcedures } from './app-procedures.js'
|
|
4
4
|
|
|
5
|
+
// @hai-generator:procedures:imports
|
|
6
|
+
|
|
5
7
|
export interface CreateServerProceduresOptions {
|
|
6
8
|
readonly app: CreateAppProceduresOptions
|
|
7
9
|
}
|
|
@@ -9,6 +11,7 @@ export interface CreateServerProceduresOptions {
|
|
|
9
11
|
/** 工程级 procedures 聚合入口;后续增加领域时在这里追加即可。 */
|
|
10
12
|
export function createServerProcedures(options: CreateServerProceduresOptions) {
|
|
11
13
|
return {
|
|
14
|
+
// @hai-generator:procedures:entries
|
|
12
15
|
app: createAppProcedures(options.app),
|
|
13
16
|
iam: { auth: createAuthProcedures() },
|
|
14
17
|
admin: createAdminProcedures(),
|