@h-ai/cli 0.1.0-alpha.60 → 0.1.0-alpha.62

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 (91) hide show
  1. package/README.en.md +25 -1
  2. package/README.ja.md +25 -1
  3. package/README.md +25 -1
  4. package/dist/{chunk-P6X63D3I.js → chunk-WW2MIO6T.js} +377 -124
  5. package/dist/chunk-WW2MIO6T.js.map +1 -0
  6. package/dist/{chunk-WWG3U5ZD.js → chunk-X3RSYNBG.js} +8 -3
  7. package/dist/chunk-X3RSYNBG.js.map +1 -0
  8. package/dist/cli-config-templates-QRF6B5T4.js +5 -0
  9. package/dist/{cli-config-templates-EFABFSCY.js.map → cli-config-templates-QRF6B5T4.js.map} +1 -1
  10. package/dist/{cli-skill-templates-2KDUACT6.js → cli-skill-templates-EZFW7D43.js} +3 -3
  11. package/dist/{cli-skill-templates-2KDUACT6.js.map → cli-skill-templates-EZFW7D43.js.map} +1 -1
  12. package/dist/index.js +31 -10
  13. package/dist/index.js.map +1 -1
  14. package/package.json +7 -7
  15. package/templates/README.md +11 -4
  16. package/templates/apps/api/.gitignore.hbs +1 -0
  17. package/templates/apps/api/e2e/capture-evidence.ts.hbs +23 -0
  18. package/templates/apps/api/playwright.config.ts.hbs +1 -0
  19. package/templates/apps/fullstack/.gitignore.hbs +1 -0
  20. package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +123 -4
  21. package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +8 -3
  22. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_ai.yml.hbs +1 -0
  23. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_audit.yml.hbs +1 -3
  24. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_cache.yml.hbs +1 -2
  25. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +7 -0
  26. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_db.yml.hbs +1 -3
  27. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_deploy.yml.hbs +1 -0
  28. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +1 -67
  29. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_payment.yml.hbs +1 -0
  30. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +31 -0
  31. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_scheduler.yml.hbs +1 -0
  32. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +10 -0
  33. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_storage.yml.hbs +1 -3
  34. package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_vecdb.yml.hbs +1 -0
  35. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +1 -21
  36. package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +1 -1
  37. package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +7 -1
  38. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +19 -1
  39. package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +19 -1
  40. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +7 -2
  41. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +5 -3
  42. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +3 -3
  43. package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +55 -0
  44. package/templates/apps/fullstack/playwright.config.ts.hbs +21 -10
  45. package/templates/apps/h5/src/routes/+layout.svelte.hbs +35 -23
  46. package/templates/base/.gitignore.hbs +1 -2
  47. package/templates/base/e2e/app.spec.ts.hbs +49 -3
  48. package/templates/base/package.json.hbs +1 -1
  49. package/templates/skills/AGENTS.md +6 -0
  50. package/templates/skills/bridges/admin/AGENTS.md +6 -0
  51. package/templates/skills/bridges/api/AGENTS.md +6 -0
  52. package/templates/skills/bridges/fullstack/AGENTS.md.hbs +6 -0
  53. package/templates/skills/bridges/h5/AGENTS.md +6 -0
  54. package/templates/skills/bridges/mobile-app/AGENTS.md +6 -0
  55. package/templates/skills/bridges/website/AGENTS.md +6 -0
  56. package/templates/skills/copilot-instructions.md +3 -9
  57. package/templates/skills/hai-app-create/SKILL.md +6 -0
  58. package/templates/skills/hai-app-create/development.md +10 -6
  59. package/templates/skills/hai-app-create/reference.md +10 -0
  60. package/templates/skills/hai-audit/SKILL.md +1 -1
  61. package/templates/skills/hai-build/SKILL.md +1 -0
  62. package/templates/skills/hai-build/reference.md +1 -1
  63. package/templates/skills/hai-ci/SKILL.md +1 -1
  64. package/templates/skills/hai-framework-sync/SKILL.md +67 -70
  65. package/templates/skills/hai-framework-sync/sync.md +49 -0
  66. package/templates/skills/hai-iam/SKILL.md +5 -0
  67. package/templates/skills/hai-iam/reference.md +88 -7
  68. package/templates/skills/hai-pr-review/SKILL.md +1 -1
  69. package/templates/skills/hai-reach/SKILL.md +1 -1
  70. package/templates/skills/hai-reach/reference.md +4 -5
  71. package/templates/skills/hai-serv/SKILL.md +1 -1
  72. package/templates/skills/hai-serv/reference.md +9 -6
  73. package/templates/skills/hai-spec-implement/SKILL.md +41 -0
  74. package/templates/skills/hai-spec-implement/templates.md +90 -0
  75. package/templates/skills/hai-spec-implement/workflow.md +53 -0
  76. package/templates/skills/hai-spec-review/SKILL.md +44 -0
  77. package/templates/skills/hai-spec-review/checklist.md +70 -0
  78. package/templates/skills/hai-storage/SKILL.md +9 -2
  79. package/templates/skills/hai-ui/SKILL.md +3 -1
  80. package/templates/skills/hai-ui/guide.md +79 -0
  81. package/templates/skills/hai-ui/reference.md +13 -0
  82. package/templates/skills/opencode.json +1 -1
  83. package/dist/chunk-P6X63D3I.js.map +0 -1
  84. package/dist/chunk-WWG3U5ZD.js.map +0 -1
  85. package/dist/cli-config-templates-EFABFSCY.js +0 -5
  86. package/templates/skills/bridges/admin/copilot-instructions.md +0 -11
  87. package/templates/skills/bridges/api/copilot-instructions.md +0 -13
  88. package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +0 -19
  89. package/templates/skills/bridges/h5/copilot-instructions.md +0 -11
  90. package/templates/skills/bridges/mobile-app/copilot-instructions.md +0 -11
  91. package/templates/skills/bridges/website/copilot-instructions.md +0 -11
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@h-ai/cli",
3
3
  "type": "module",
4
- "version": "0.1.0-alpha.60",
4
+ "version": "0.1.0-alpha.62",
5
5
  "description": "Hai Framework CLI for project scaffolding and code generation.",
6
6
  "license": "Apache-2.0",
7
7
  "keywords": [
@@ -30,7 +30,7 @@
30
30
  "node": "^22.22.2 || ^24.15.0 || >=26.0.0"
31
31
  },
32
32
  "dependencies": {
33
- "@h-ai/core": "0.1.0-alpha.60",
33
+ "@h-ai/core": "0.1.0-alpha.62",
34
34
  "cac": "^7.0.1",
35
35
  "chalk": "^6.0.1",
36
36
  "fast-wrap-ansi": "^0.2.2",
@@ -41,11 +41,11 @@
41
41
  "yaml": "^2.9.1"
42
42
  },
43
43
  "devDependencies": {
44
- "@h-ai/cache": "0.1.0-alpha.60",
45
- "@h-ai/deploy": "0.1.0-alpha.60",
46
- "@h-ai/reach": "0.1.0-alpha.60",
47
- "@h-ai/reldb": "0.1.0-alpha.60",
48
- "@h-ai/storage": "0.1.0-alpha.60",
44
+ "@h-ai/cache": "0.1.0-alpha.62",
45
+ "@h-ai/deploy": "0.1.0-alpha.62",
46
+ "@h-ai/reach": "0.1.0-alpha.62",
47
+ "@h-ai/reldb": "0.1.0-alpha.62",
48
+ "@h-ai/storage": "0.1.0-alpha.62",
49
49
  "@types/fs-extra": "^11.0.4",
50
50
  "@types/node": "^26.6.5",
51
51
  "@types/prompts": "^2.4.9",
@@ -57,7 +57,7 @@ templates/
57
57
  │ └── settings.json # Paraglide 项目配置
58
58
  │
59
59
  └── skills/ # AI Skill 与各 appType 桥接指引
60
- └── bridges/ # admin / api / website / h5 / mobile-app / fullstack 专属 AGENTS/Copilot
60
+ └── bridges/ # admin / api / website / h5 / mobile-app / fullstack 专属 AGENTS
61
61
  ```
62
62
 
63
63
  ## AI Skill 模板(Single Source of Truth)
@@ -65,11 +65,12 @@ templates/
65
65
  `templates/skills/` 是 CLI 生成共享 AI 上下文时的单一来源,同时用于 `hai create` 与 `hai add`。
66
66
 
67
67
  - `hai create` 会把 `templates/skills/` 下所有 `hai-*` Skill 目录复制到 `.agents/skills/<skill>/`,再按 appType 排除互斥项(例如 fullstack 不复制 `hai-kit`;admin / website / h5 不复制 `hai-serv`、`hai-api-contract`、`hai-api-client`、`hai-capacitor`;mobile-app 不复制 `hai-core`、`hai-kit`、`hai-serv`、`hai-api-contract`)
68
- - `bridges/<appType>/copilot-instructions.md` 会复制到 `.github/copilot-instructions.md`;没有专属文件时回退到通用桥接文件
68
+ - 通用 `copilot-instructions.md` 复制到 `.github/copilot-instructions.md`,只引用项目根 `AGENTS.md`,不另维护 appType 专属规则
69
69
  - `bridges/<appType>/AGENTS.md` 会复制到项目根目录,确保不同样板工程使用自己的 AI 指引;通用 `CLAUDE.md` 仅包含 `@AGENTS.md`
70
70
  - `opencode.json` 会复制到项目根目录
71
- - `templates/skills/AGENTS.md`、`CLAUDE.md`、`copilot-instructions.md` 与 `opencode.json` 不是多余副本:它们是 generic fallback,主要服务于缺少 appType 上下文时的桥接复制与 `hai add` 回填 AI 支持
72
- - `opencode.json` 使用 OpenCode 的 `instructions` 配置复用 `.github/copilot-instructions.md`;`.agents/skills/` 由 OpenCode 原生发现,不再写入过期的 `skills.paths`
71
+ - `templates/skills/AGENTS.md` 是缺少 appType 上下文时的通用规则;`CLAUDE.md`、`copilot-instructions.md` 与 `opencode.json` 是各工具的轻量入口,用于创建项目和回填 AI 支持
72
+ - Codex 原生读取根 `AGENTS.md` 并发现 `.agents/skills/`,支持 `$hai-app-create` 等显式调用和按任务自动选择;无需 `.codex/skills` 或工具专属配置。依据:[Codex skills](https://learn.chatgpt.com/docs/build-skills)、[AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md)
73
+ - `opencode.json` 使用 OpenCode 的 `instructions` 配置直接引用 `AGENTS.md`;`.agents/skills/` 由 OpenCode 原生发现,不再写入过期的 `skills.paths`
73
74
  - `CLAUDE.md` 通过 `@AGENTS.md` 复用共享规范,Claude Code 按其中的任务导航读取 `.agents/skills/` 正文与参考。CLI 不生成 `.claude/` 技能副本;此模式不提供 Claude 原生技能自动发现或 `/skill` 命令。依据:[Claude Code 项目指引](https://code.claude.com/docs/en/memory)、[原生 skills](https://code.claude.com/docs/en/skills)
74
75
  - `hai add` / `hai init` 只补齐缺失的 AI 支持文件,不覆盖用户已自定义的桥接文件,也不会自动删除遗留的 `.github/skills/` 或 `.claude/`
75
76
 
@@ -104,8 +105,14 @@ templates/
104
105
 
105
106
  `appType` 支持 `admin` / `website` / `h5` / `api` / `mobile-app` / `fullstack`。
106
107
 
108
+ H5 业务页面复用 `@h-ai/ui` 的 `BottomNav` 和移动端壳样式,桌面预览保持居中限宽,认证路由独立显示。
109
+
107
110
  `fullstack.*` 包含包名、前端选择、依赖版本、`contractExportName` 和原生壳 `nativeAppIdSegment`,用于渲染 contract / serv / 多端 UI / Capacitor / Tauri 模板。
108
111
 
112
+ `moduleConfigs` 保留创建时指定的模块值。Fullstack 的模块 YAML 用 `{{{moduleConfig 'db' true}}}` 等 helper 复用 `cli-config-templates.ts`;可选模块传 `features.ai` 等开关,未选择时整份注释。创建流程保留已渲染的配置,不再用精简内容覆盖;core、serv、Reach 保留应用专用默认值及完整的可选字段。新增 Schema 字段同步此生成器和相应模板。
113
+
114
+ 所有根 `.gitignore`(base / api / fullstack)保留 `**/data/`,包括 `.gitkeep` 在内的运行数据不跟踪。Fullstack 服务端 config 始终含 core、serv、db、cache、iam、storage、audit、reach、ai、vecdb、payment、scheduler、deploy 共 13 份 YAML;不为无配置入口的 crypto/datapipe 生成 YAML。未启用块的后续接入步骤见 [CLI 配置说明](../README.md#应用配置与显示)。
115
+
109
116
  ### 常用条件写法
110
117
 
111
118
  ```handlebars
@@ -1,4 +1,5 @@
1
1
  node_modules
2
+ **/data/
2
3
  .turbo
3
4
  dist
4
5
  build
@@ -0,0 +1,23 @@
1
+ import { mkdir } from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import process from 'node:process'
4
+ import { chromium, expect } from '@playwright/test'
5
+
6
+ /** 仅在指定证据目录时采集健康端点截图,常规 API 测试通过 HTTP 请求完成。 */
7
+ export default async function captureEvidence() {
8
+ const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
9
+ if (!evidence)
10
+ return
11
+ const browser = await chromium.launch({ channel: 'chrome' })
12
+ try {
13
+ const page = await browser.newPage()
14
+ const response = await page.goto(new URL('/health', process.env.BASE_URL || 'http://127.0.0.1:3000').href)
15
+ expect(response?.status()).toBe(200)
16
+ await expect(page.locator('body')).toContainText('ok')
17
+ await mkdir(evidence, { recursive: true })
18
+ await page.screenshot({ path: path.join(evidence, '{{projectName}}-health.png'), fullPage: true })
19
+ }
20
+ finally {
21
+ await browser.close()
22
+ }
23
+ }
@@ -32,6 +32,7 @@ function resolvePortFromBaseUrl(urlString: string, fallbackPort: string): string
32
32
 
33
33
  export default defineConfig({
34
34
  testDir: './e2e',
35
+ globalSetup: './e2e/capture-evidence.ts',
35
36
  fullyParallel: false,
36
37
  forbidOnly: !!process.env.CI,
37
38
  retries: process.env.CI ? 2 : 0,
@@ -1,4 +1,5 @@
1
1
  node_modules
2
+ **/data/
2
3
  .turbo
3
4
  dist
4
5
  build
@@ -18,13 +18,29 @@ const configuredEnv = process.env.HAI_CORE_ENV || 'test'
18
18
  // 测试专用的一像素 PNG;上传仍使用完整前后端加密链路。
19
19
  const avatar = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aSxkAAAAASUVORK5CYII=', 'base64')
20
20
 
21
- async function login(page: Page, username: string, password: string) {
21
+ async function capture(page: Page, name: string, fallback: string) {
22
+ const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
23
+ if (evidence)
24
+ await mkdir(evidence, { recursive: true })
25
+ await expect(page.getByTestId('guide-loading')).toHaveCount(0)
26
+ await page.evaluate(() => document.fonts.ready)
27
+ await page.screenshot({ path: evidence ? path.join(evidence, name) : fallback, fullPage: true, animations: 'disabled' })
28
+ }
29
+
30
+ async function submitLogin(page: Page, username: string, password: string) {
22
31
  await expect(page.getByTestId('login-form')).toBeVisible()
23
32
  await page.locator('#login-username').fill(username)
24
33
  await page.getByTestId('login-form').locator('input[type="password"]').fill(password)
25
34
  await page.getByTestId('login-form').getByRole('button', { name: /登录|Sign in|Login|Log in/i }).click()
26
35
  }
27
36
 
37
+ async function login(page: Page, username: string, password: string, keepGuide = false) {
38
+ await submitLogin(page, username, password)
39
+ await expect(page.getByTestId('logout')).toBeVisible()
40
+ if (!keepGuide && await page.getByTestId('guide-later').isVisible())
41
+ await page.getByTestId('guide-later').click()
42
+ }
43
+
28
44
  async function userEditor(page: Page, username: string) {
29
45
  await page.getByTestId('users-search').locator('input').fill(username)
30
46
  await page.getByTestId('users-page').getByRole('button', { name: /查询|Search/ }).click()
@@ -45,6 +61,95 @@ test('service rejects plaintext business requests', async ({ request }) => {
45
61
  })
46
62
 
47
63
  {{#if fullstack.e2eFrontend}}
64
+ test('first-use tour, help search and settings navigation work on desktop and mobile', async ({ page }, testInfo) => {
65
+ await page.goto('/')
66
+ await login(page, 'admin', adminPassword, true)
67
+ await expect(page.getByTestId('guide-offer')).toBeVisible()
68
+ await page.getByTestId('guide-start').click()
69
+ const popover = page.locator('.hai-guide-popover')
70
+ await expect(popover).toBeVisible()
71
+ await capture(page, '{{projectName}}-guide-desktop.png', testInfo.outputPath('guide-desktop.png'))
72
+ for (let step = 1; step < 3; step++) {
73
+ await popover.locator('.driver-popover-next-btn').click()
74
+ await expect(popover.locator('.driver-popover-progress-text')).toContainText(String(step + 1))
75
+ }
76
+ await popover.locator('.driver-popover-next-btn').click()
77
+ await expect(popover).toHaveCount(0)
78
+ await page.reload()
79
+ await expect(page.getByTestId('admin-help')).toBeVisible()
80
+ await expect(page.getByTestId('guide-offer')).toHaveCount(0)
81
+ await page.getByTestId('admin-help').click()
82
+ const profileTitle = (await page.getByTestId('guide-article-profile').textContent())!.trim()
83
+ await page.getByTestId('guide-search').locator('input').fill(profileTitle)
84
+ await expect(page.getByTestId('guide-article-profile')).toBeVisible()
85
+ await expect(page.getByTestId('guide-article-overview')).toHaveCount(0)
86
+ await page.getByTestId('guide-article-start').click()
87
+ await expect(page.getByTestId('settings-page')).toBeVisible()
88
+ await expect(popover).toBeVisible()
89
+ await page.keyboard.press('Escape')
90
+ await expect(popover).toHaveCount(0)
91
+ await page.setViewportSize({ width: 390, height: 844 })
92
+ await page.getByTestId('admin-help').click()
93
+ await expect(page.getByTestId('help-center')).toBeVisible()
94
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
95
+ await capture(page, '{{projectName}}-guide-help-mobile.png', testInfo.outputPath('guide-help-mobile.png'))
96
+ await page.getByTestId('guide-tour-overview').click()
97
+ await expect(popover.locator('.driver-popover-next-btn')).toBeEnabled()
98
+ // 下一步未渲染时仍能通过宿主提示取消,不能被引擎遮罩阻止。
99
+ await page.locator('[data-guide="admin-profile"]').evaluate(element => element.removeAttribute('data-guide'))
100
+ await popover.locator('.driver-popover-next-btn').click()
101
+ await expect(page.getByTestId('guide-loading')).toBeVisible()
102
+ await page.getByTestId('guide-cancel').click()
103
+ await expect(popover).toHaveCount(0)
104
+ await expect(page.getByTestId('guide-error')).toHaveCount(0)
105
+ })
106
+
107
+ // 所有已生成的浏览器前端都验证共享引导,不能用 Web 的成功代替 App/Desktop 预览。
108
+ const additionalFrontends = [
109
+ {{#each fullstack.frontendApps}}
110
+ {
111
+ target: '{{target}}',
112
+ url: process.env.HAI_E2E_{{target}}_URL || 'http://127.0.0.1:{{port}}',
113
+ },
114
+ {{/each}}
115
+ ].filter(frontend => frontend.target !== '{{fullstack.e2eFrontend.target}}')
116
+
117
+ for (const frontend of additionalFrontends) {
118
+ test(`${frontend.target} preview supports login, tour, help and profile navigation`, async ({ browser }, testInfo) => {
119
+ const page = await browser.newPage({ viewport: frontend.target === 'app' ? { width: 390, height: 844 } : { width: 1440, height: 960 } })
120
+ const pageErrors: string[] = []
121
+ page.on('pageerror', error => pageErrors.push(error.message))
122
+ try {
123
+ await page.goto(frontend.url)
124
+ await expect(page.getByTestId('login-form')).toBeVisible()
125
+ await capture(page, `{{projectName}}-${frontend.target}-login.png`, testInfo.outputPath(`${frontend.target}-login.png`))
126
+ await login(page, 'admin', adminPassword, true)
127
+ await expect(page.getByTestId('guide-offer')).toBeVisible()
128
+ await page.getByTestId('guide-start').click()
129
+ const popover = page.locator('.hai-guide-popover')
130
+ await expect(popover).toBeVisible()
131
+ await capture(page, `{{projectName}}-${frontend.target}-guide.png`, testInfo.outputPath(`${frontend.target}-guide.png`))
132
+ await page.keyboard.press('Escape')
133
+ await expect(popover).toHaveCount(0)
134
+ await page.getByTestId('admin-help').click()
135
+ await expect(page.getByTestId('help-center')).toBeVisible()
136
+ expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
137
+ await capture(page, `{{projectName}}-${frontend.target}-help.png`, testInfo.outputPath(`${frontend.target}-help.png`))
138
+ await page.getByTestId('guide-article-profile').click()
139
+ await page.getByTestId('guide-article-start').click()
140
+ await expect(page.getByTestId('settings-page')).toBeVisible()
141
+ await expect(popover).toBeVisible()
142
+ await page.keyboard.press('Escape')
143
+ await expect(popover).toHaveCount(0)
144
+ await capture(page, `{{projectName}}-${frontend.target}-profile.png`, testInfo.outputPath(`${frontend.target}-profile.png`))
145
+ expect(pageErrors).toEqual([])
146
+ }
147
+ finally {
148
+ await page.close()
149
+ }
150
+ })
151
+ }
152
+
48
153
  test('shared authentication supports public registration with configuration', async ({ page }) => {
49
154
  test.setTimeout(90_000)
50
155
  const evidence = process.env.HAI_AUTH_EVIDENCE_DIR || path.resolve('test-results/auth-screenshots')
@@ -106,7 +211,7 @@ test('shared authentication supports public registration with configuration', as
106
211
  await expect(page.getByTestId('settings-page')).toBeVisible()
107
212
  })
108
213
 
109
- test('frontend displays configured core metadata and restores the administrator session', async ({ page }) => {
214
+ test('frontend displays configured core metadata and restores the administrator session', async ({ page }, testInfo) => {
110
215
  await page.goto('/')
111
216
  await expect(page.getByTestId('app-name')).toHaveText(configuredName)
112
217
  await expect(page).toHaveTitle(configuredName)
@@ -118,17 +223,24 @@ test('frontend displays configured core metadata and restores the administrator
118
223
  await expect(page.getByTestId('users-page')).toBeVisible()
119
224
  await expect(page.getByTestId('nav-roles')).toBeVisible()
120
225
  await expect(page.getByTestId('nav-audit')).toBeVisible()
226
+ const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
227
+ if (evidence)
228
+ await mkdir(evidence, { recursive: true })
229
+ await page.screenshot({ path: evidence ? path.join(evidence, '{{projectName}}-users.png') : testInfo.outputPath('users.png'), fullPage: true })
121
230
  await page.getByTestId('logout').click()
122
231
  await expect(page.getByTestId('login-form')).toBeVisible()
123
232
  })
124
233
 
125
- test('audit filters align and fit desktop, tablet and narrow mobile screens', async ({ page }) => {
234
+ test('audit filters align and fit desktop, tablet and narrow mobile screens', async ({ page }, testInfo) => {
126
235
  await page.goto('/')
127
236
  await login(page, 'admin', adminPassword)
128
237
  await page.getByTestId('nav-audit').click()
129
238
  await expect(page.getByTestId('audit-page')).toBeVisible()
130
239
  for (const width of [1440, 960, 390, 320]) {
131
240
  await page.setViewportSize({ width, height: 900 })
241
+ // 等待移动侧栏收起,避免过渡帧遮挡内容并污染截图。
242
+ if (width < 1024)
243
+ await expect.poll(() => page.locator('aside').evaluate(sidebar => sidebar.getBoundingClientRect().right)).toBeLessThanOrEqual(1)
132
244
  const controls = await page.getByTestId('audit-page').locator('form').evaluate(form => [
133
245
  form.querySelector('#audit-action')?.parentElement,
134
246
  form.querySelector('#audit-resource')?.parentElement,
@@ -152,6 +264,13 @@ test('audit filters align and fit desktop, tablet and narrow mobile screens', as
152
264
  expect(controls[2]!.top).toBeCloseTo(controls[3]!.top, 0)
153
265
  }
154
266
  expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true)
267
+ if (width === 1440 || width === 390) {
268
+ const evidence = process.env.HAI_AUTH_EVIDENCE_DIR
269
+ if (evidence)
270
+ await mkdir(evidence, { recursive: true })
271
+ const file = `{{projectName}}-audit-${width}.png`
272
+ await page.screenshot({ path: evidence ? path.join(evidence, file) : testInfo.outputPath(file), fullPage: true, animations: 'disabled' })
273
+ }
155
274
  }
156
275
  })
157
276
 
@@ -252,7 +371,7 @@ test('administrator and user complete account, avatar, preference, password and
252
371
  await passwordInputs.nth(2).fill(newPassword)
253
372
  await userPage.getByTestId('password-form').getByRole('button', { name: /Change password/i }).click()
254
373
  await expect(userPage.getByTestId('login-form')).toBeVisible()
255
- await login(userPage, username, password)
374
+ await submitLogin(userPage, username, password)
256
375
  await expect(userPage.getByTestId('login-form')).toContainText(/不正确|incorrect/i)
257
376
  await login(userPage, username, newPassword)
258
377
  await expect(userPage.getByTestId('settings-page')).toBeVisible()
@@ -6,12 +6,13 @@
6
6
 
7
7
  - `config/_core.yml`:应用元信息(名称 / 版本 / 环境 / 语言),启动时由 `core.config` 加载。
8
8
  - `config/_serv.yml`:服务端运行时设置(CORS 来源等)。
9
- - `config/_db.yml` / `_cache.yml` / `_iam.yml` / `_storage.yml` / `_audit.yml`:mini 管理所需模块配置,初始化前使用模块公开 Schema 验证。
10
- - `src/index.ts`:进程入口,先 `core.init({ configDir: './config' })`,再初始化 reldb → cache → crypto → storage → iam → audit;启动失败及退出反序释放。
9
+ - `config/_db.yml` / `_cache.yml` / `_iam.yml` / `_storage.yml` / `_audit.yml` / `_reach.yml`:管理及开发通知配置,初始化前使用模块公开 Schema 验证。Reach 默认控制台 Provider,生产按注释替换为 SMTP/短信/API。
10
+ - `config/_ai.yml` / `_vecdb.yml` / `_payment.yml` / `_scheduler.yml` / `_deploy.yml`:保留未启用模块的完整注释示例;配置文件存在不代表模块已接入。先在本 workspace 安装对应依赖,再在 `src/server/runtime.ts` 按公开 Schema 校验、初始化并反序关闭,接入实际业务后按需取消注释。整份注释先去掉一层外层注释,可选字段继续按需开启。
11
+ - `src/index.ts`:进程入口,先 `core.init({ configDir: './config' })`,再初始化 reldb → cache → crypto → storage → reach(有配置时)→ iam → audit;启动失败及退出反序释放。
11
12
  - `src/app-server.ts`:服务装配入口,挂载健康检查、OpenAPI、docs 与 procedures;应用名称 / 版本与 CORS 来源均取自上述配置。
12
13
  - `src/server/procedures/app-procedures.ts`:app 领域 procedures,对应 contract 中的 `app.info` / `app.echo` 路由。
13
14
  - `src/server/procedures/admin-procedures.ts`:认证、管理员用户/角色/审计管理和个人设置,使用 `.auth()` / `.role('admin')` 守卫。每个请求读取用户最新启用状态、角色和权限。
14
- - 当前用户与刷新 procedures 直接选用 `@h-ai/serv/features/iam` 的默认实现;仅覆写登录、退出、修改密码以追加业务审计。未选的注册/OTP/通用管理接口不挂载,HTTP App 类型直接复用公开 `ServHttpApp`。
15
+ - 标准认证 procedures 复用 `@h-ai/serv/features/iam`,注册、登录方式、重置与协议由 `_iam.yml` 控制;登录、退出、修改密码追加业务审计。HTTP App 类型复用公开 `ServHttpApp`。
15
16
  - `src/server/admin-service.ts`:角色分配、头像上传和审计编排,全部底层操作复用 hai 模块。固定 admin/user 两种角色;锁内重新鉴权,禁止管理员自删、自禁用、自降级,并补偿用户更新失败后的角色变更。
16
17
  - `tests/app-server.test.ts`:真实加密 HTTP 测试,覆盖 Cookie/受信原生刷新、RBAC、旧会话权限、并发管理操作、补偿、头像安全、偏好、审计及会话失效。
17
18
 
@@ -23,6 +24,10 @@ CLI 已生成根目录 `.env`,`HAI_ADMIN_DEFAULT_PASSWORD` 为每个项目独
23
24
 
24
25
  ## 开发
25
26
 
27
+ 所有配置字段以模块 Schema 为准;未使用字段和替代 Provider 用注释保留。叶子环境变量只覆盖已有 YAML 路径,先取消相应键的注释,或用 `HAI_DB` / `HAI_CACHE` 等整体对象覆盖;`connectTimeout` 对应 `HAI_CACHE_CONNECTTIMEOUT`。凭据示例在根 `.env.example`,实际密钥保存在忽略的 `.env` 或部署环境。`.gitignore` 的 `**/data/` 忽略所有运行数据及 `.gitkeep`;运行时会创建目录。`crypto.init()` 无配置参数,datapipe 为纯函数,不提供虚构配置。
28
+
29
+ 自动生成的 `.env` 只启用初始管理员密码和 `PUBLIC_API_BASE`;模块环境覆盖保留注释,以 YAML 为默认值来源。有明确环境覆盖时编辑 YAML 要同步核对。
30
+
26
31
  - `pnpm --filter {{fullstack.servPackageName}} dev`
27
32
  - `pnpm --filter {{fullstack.servPackageName}} test`
28
33
 
@@ -0,0 +1 @@
1
+ {{{moduleConfig 'ai' features.ai}}}
@@ -1,3 +1 @@
1
- userTable: hai_iam_users
2
- userIdColumn: id
3
- userNameColumn: username
1
+ {{{moduleConfig 'audit' true}}}
@@ -1,2 +1 @@
1
- # 多实例部署请使用共享 Redis,保障会话、管理员变更锁一致。
2
- type: memory
1
+ {{{moduleConfig 'cache' true}}}
@@ -15,7 +15,14 @@ defaultLocale: {{fullstack.i18n.baseLocale}}
15
15
  logging:
16
16
  level: info
17
17
  format: json
18
+ # context:
19
+ # service: api
18
20
  redact:
19
21
  - password
20
22
  - token
21
23
  - secret
24
+
25
+ # 短 ID 生成配置;未设置时采用框架默认值。
26
+ # id:
27
+ # prefix: app_
28
+ # length: 21
@@ -1,3 +1 @@
1
- # 默认单实例 SQLite;HAI_DB 可整体替换为 PostgreSQL/MySQL 配置。
2
- type: sqlite
3
- database: ./data/app.db
1
+ {{{moduleConfig 'db' true}}}
@@ -0,0 +1 @@
1
+ {{{moduleConfig 'deploy' features.deploy}}}
@@ -1,67 +1 @@
1
- login:
2
- password: true
3
- otp: false
4
- ldap: false
5
- apikey: false
6
- password:
7
- minLength: 8
8
- maxLength: 128
9
- requireUppercase: false
10
- requireLowercase: false
11
- requireNumber: false
12
- requireSpecialChar: false
13
- session:
14
- maxAge: 86400
15
- sliding: true
16
- singleDevice: false
17
- refreshTokenMaxAge: 604800
18
- rbac:
19
- defaultRole: user
20
- # 仅由应用创建 admin/user,避免生成无业务用途的默认角色与权限。
21
- seedDefaultData: false
22
-
23
- # 注册与登录安全
24
- register:
25
- enabled: true
26
- defaultEnabled: true
27
- security:
28
- maxLoginAttempts: 5
29
- lockoutDuration: 900
30
-
31
- # LDAP 可选:安装 ldapts 后开启 login.ldap,并声明 ldap 连接配置或注入工厂。
32
- # OTP 需启用 login.otp,并选择 Reach 标准模板或自定义发送回调。
33
- otp:
34
- email: true
35
- sms: true
36
- length: 6
37
- expiresIn: 300
38
- maxAttempts: 3
39
- resendInterval: 60
40
- passwordReset:
41
- enabled: true
42
- mode: email # phone 使用 password_reset_sms 模板
43
- tokenExpiresIn: 3600
44
- maxAttempts: 3
45
- # resetUrl: https://example.com/auth/reset-password
46
- agreements:
47
- showOnRegister: true
48
- showOnLogin: false
49
- # Markdown 正文由公开配置传给所有客户端,在弹窗内安全渲染。
50
- userAgreementMarkdown: |
51
- # 用户协议示例
52
-
53
- 请妥善保管账户凭据,并按应用提供的功能使用服务。
54
- privacyPolicyMarkdown: |
55
- # 隐私政策示例
56
-
57
- 注册资料用于账户管理,会话信息用于身份认证。
58
- # userAgreementUrl: https://example.com/terms
59
- # privacyPolicyUrl: https://example.com/privacy
60
- ui:
61
- variant: card
62
- # title: Account center
63
- # subtitle: Welcome back
64
- # brandTitle: Your application
65
- # brandText: Sign in securely
66
- # logoUrl: /logo.svg
67
- # illustrationUrl: /auth-illustration.svg
1
+ {{{moduleConfig 'iam' true}}}
@@ -0,0 +1 @@
1
+ {{{moduleConfig 'payment' features.payment}}}
@@ -4,6 +4,30 @@ providers:
4
4
  type: console
5
5
  - name: sms
6
6
  type: console
7
+ # SMTP 替换 email 控制台 Provider;同名 Provider 只保留一个。
8
+ # - name: email
9
+ # type: smtp
10
+ # host: ${HAI_REACH_SMTP_HOST:smtp.example.com}
11
+ # port: ${HAI_REACH_SMTP_PORT:465}
12
+ # secure: true
13
+ # user: ${HAI_REACH_SMTP_USER:}
14
+ # pass: ${HAI_REACH_SMTP_PASS:}
15
+ # from: ${HAI_REACH_SMTP_FROM:noreply@example.com}
16
+ # 阿里云短信替换 sms 控制台 Provider。
17
+ # - name: sms
18
+ # type: aliyun-sms
19
+ # accessKeyId: ${HAI_REACH_SMS_ACCESS_KEY}
20
+ # accessKeySecret: ${HAI_REACH_SMS_SECRET_KEY}
21
+ # signName: MyApp
22
+ # endpoint: dysmsapi.aliyuncs.com
23
+ # 自有发送服务:按需将 template.provider 改为 custom。
24
+ # - name: custom
25
+ # type: api
26
+ # url: https://notify.example.com/send
27
+ # method: POST # POST | PUT
28
+ # headers:
29
+ # Authorization: 'Bearer ${HAI_REACH_API_TOKEN}'
30
+ # timeout: 10000
7
31
  templates:
8
32
  - name: otp_email
9
33
  provider: email
@@ -19,3 +43,10 @@ templates:
19
43
  - name: password_reset_sms
20
44
  provider: sms
21
45
  body: 'Reset your password: {resetUrl}. Expires at {expiresAt}.'
46
+
47
+ # 免打扰可选;验证码应避免 delay,以免延迟到失效后投递。
48
+ # dnd:
49
+ # enabled: false
50
+ # strategy: discard # discard | delay
51
+ # start: '22:00'
52
+ # end: '08:00'
@@ -0,0 +1 @@
1
+ {{{moduleConfig 'scheduler' features.scheduler}}}
@@ -10,15 +10,24 @@ server:
10
10
  port: 3000
11
11
 
12
12
  http:
13
+ # 仅列出会清洗/追加转发头的代理 IP/CIDR;空数组忽略 X-Forwarded-For/X-Real-IP。
14
+ trustedProxies: []
13
15
  apiPrefix: /api/v1
14
16
  openapi:
15
17
  path: /openapi.json
16
18
  docs:
17
19
  path: /docs
20
+ # requireAuth: true
18
21
  health:
19
22
  path: /health
20
23
  readyPath: /ready
21
24
  rpc: false
25
+ # 内部 RPC 可选;启用时用对象替换 false,gateway-only 必须配置网关密钥。
26
+ # rpc:
27
+ # prefix: /api/rpc
28
+ # access: gateway-only # loopback | private-network | gateway-only
29
+ # gatewayHeader: X-Gateway-Key
30
+ # gatewaySecret: ${HAI_SERV_HTTP_RPC_GATEWAYSECRET}
22
31
 
23
32
  cors:
24
33
  origin: ${HAI_CORS_ORIGIN:*}
@@ -40,3 +49,4 @@ cors:
40
49
  transport:
41
50
  keyExchangePath: /_hai/key-exchange
42
51
  maxClients: 10000
52
+ # excludePaths: [/health, /ready]
@@ -1,3 +1 @@
1
- # HAI_STORAGE 可整体替换为 S3/MinIO/RustFS 配置。
2
- type: local
3
- root: ./data/uploads
1
+ {{{moduleConfig 'storage' true}}}
@@ -0,0 +1 @@
1
+ {{{moduleConfig 'vecdb' features.vecdb}}}
@@ -1,6 +1,6 @@
1
1
  import type { CoreConfig } from '@h-ai/core'
2
2
  import type { ServHttpApp } from '@h-ai/serv'
3
- import { core, err, HaiCommonError, ok } from '@h-ai/core'
3
+ import { core } from '@h-ai/core'
4
4
  import { createReldbTransportKeyStore, crypto } from '@h-ai/crypto'
5
5
  import { iam } from '@h-ai/iam'
6
6
  import { reldb } from '@h-ai/reldb'
@@ -40,26 +40,6 @@ export function createServerApp(): ServHttpApp {
40
40
  },
41
41
  }),
42
42
  iam,
43
- verifyToken: async (token) => {
44
- const session = await iam.session.verifyToken(token)
45
- if (!session.success)
46
- return session
47
- const user = await iam.user.getUser(session.data.userId)
48
- if (!user.success)
49
- return user
50
- if (!user.data?.enabled)
51
- return err(HaiCommonError.UNAUTHORIZED, core.i18n.coreM('core_errorUnauthorized'))
52
- // 会话中的授权同步属于最大努力,业务守卫始终读取最新持久化角色与权限。
53
- const [roles, permissions] = await Promise.all([
54
- iam.authz.getUserRoles(user.data.id),
55
- iam.authz.getUserPermissions(user.data.id),
56
- ])
57
- if (!roles.success)
58
- return roles
59
- if (!permissions.success)
60
- return permissions
61
- return ok({ ...session.data, roles: roles.data.map(role => role.code), permissions: permissions.data.map(permission => permission.code) })
62
- },
63
43
  refreshCookie: {
64
44
  secure: security.secureRefreshCookie,
65
45
  nativeTokenTransport: { isRequest: request => request.headers.get('x-hai-token-transport') === 'body'
@@ -85,7 +85,7 @@ export function createAdminProcedures() {
85
85
  const result = await audit.list({ ...input, startDate: input.startDate ? new Date(input.startDate) : undefined, endDate: input.endDate ? new Date(input.endDate) : undefined })
86
86
  if (!result.success)
87
87
  return result
88
- return ok({ ...result.data, page: input.page, pageSize: input.pageSize, items: result.data.items.map(item => ({ ...item, createdAt: new Date(item.createdAt).toISOString() })) })
88
+ return ok({ ...result.data, items: result.data.items.map(item => ({ ...item, createdAt: new Date(item.createdAt).toISOString() })) })
89
89
  }).build()
90
90
  }
91
91
 
@@ -21,6 +21,12 @@ Capacitor native uses `createCapacitorTokenStorage()` and the trusted native tok
21
21
 
22
22
  Language and theme preferences are saved in IAM user metadata, restored after login and synchronized with the shared/host Paraglide runtimes and `core.i18n`. The small reactive theme store delegates DOM updates and safe appearance caching to `@h-ai/ui/theme` (`applyTheme` / `getSavedTheme`); the framework uses the `theme` localStorage key. Appearance selectors live in personal settings and save through the authenticated profile contract.
23
23
 
24
+ ## Onboarding and help
25
+
26
+ AdminWorkspace mounts the shared `@h-ai/ui/guide` controller and GuideHost after authentication. AppShell includes a help button; first-use invitations offer start, later and dismiss. Web stores preferences in Cookies, while App/Desktop use localStorage, scoped by the current user and tour version. Help articles link to the overview and profile tours.
27
+
28
+ Extend `src/lib/stores/guide-store.ts` and the shared Chinese/English messages, then add stable data-guide attributes to existing components. Navigation uses the same ManagementStore. Keep business text as i18n callbacks and preserve the host's locale/scope binding. Browser tests cover completion, reload, replay, settings navigation and mobile help.
29
+
24
30
  ## i18n and themes
25
31
 
26
32
  Shared page copy belongs in `messages/{zh-CN,en-US}.json`. Frontend-specific copy belongs in each app's own messages directory. Keep `baseLocale` and `locales` aligned across all enabled projects, then run root `pnpm i18n:compile`; do not edit generated Paraglide files.
@@ -31,7 +37,7 @@ The scaffold theme configuration determines the themes compiled into frontend CS
31
37
 
32
38
  ## Validation
33
39
 
34
- Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Shared API tests exercise real transport key exchange, encrypted request/response handling, configured core metadata and credential forwarding. Root browser tests cover administrator and ordinary-user workflows, avatar rendering, persisted preferences, password/session invalidation and narrow-screen layout.
40
+ Run root `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm test` and `pnpm test:e2e`. Shared API tests exercise real transport key exchange, encrypted request/response handling, configured core metadata and credential forwarding. Root browser tests cover administrator and ordinary-user workflows, avatar rendering, persisted preferences, password/session invalidation and narrow-screen layout. All selected Web/App/Desktop previews also exercise login, tours, help and profile navigation. Set `HAI_AUTH_EVIDENCE_DIR` to keep screenshots outside the test output directory. These browser checks do not replace native-shell or miniapp device acceptance.
35
41
 
36
42
  ## IAM configuration
37
43
 
@@ -83,5 +83,23 @@
83
83
  "audit_system": "System",
84
84
  "audit_details": "Audit log details",
85
85
  "audit_date_invalid": "The end date must be on or after the start date.",
86
- "api_not_configured": "The service connection is not configured. Please contact your administrator."
86
+ "api_not_configured": "The service connection is not configured. Please contact your administrator.",
87
+ "guide_overview_title": "Explore your admin workspace",
88
+ "guide_overview_description": "A few short steps introduce the workspace, personal settings and help. Leave at any time and replay tours from the help center.",
89
+ "guide_content_title": "Workspace",
90
+ "guide_content_description": "The current page appears here. Use navigation to switch features. Available actions depend on your permissions.",
91
+ "guide_theme_title": "Appearance",
92
+ "guide_theme_description": "Switch between light and dark appearances here. More themes are available in settings.",
93
+ "guide_profile_title": "Your profile",
94
+ "guide_profile_description": "Use your avatar to access your profile and settings, including personal information and passwords.",
95
+ "guide_help_title": "Help center",
96
+ "guide_help_description": "Search instructions here whenever you need help, or replay a product tour.",
97
+ "guide_profile_tour_title": "Set up your profile",
98
+ "guide_profile_tour_description": "Find your personal information, preferences and password settings. This tour explains actions without submitting changes.",
99
+ "guide_profile_form_description": "View or edit your profile, choose an avatar and save your changes. Uploaded files must meet the format and size requirements shown on the page.",
100
+ "guide_preferences_description": "Save your preferred language and theme. The interface and help content follow your selection.",
101
+ "guide_password_title": "Change password",
102
+ "guide_password_description": "Enter your current password, new password and confirmation. Check the password rules before submitting.",
103
+ "guide_overview_article": "## Getting started\n\n- Open features through navigation. Menus and actions depend on your current permissions.\n- The workspace contains lists, details or settings. Follow the page instructions and check information before submitting.\n- Use your avatar to access your profile and settings.\n- Search instructions or replay tours through the help button.\n\n## First-use tour\n\n“Remind me later” pauses reminders for one day. “Do not remind me” saves your preference. Completed tours no longer prompt automatically and remain available for manual replay.",
104
+ "guide_profile_article": "## Profile and preferences\n\nOpen your profile to view or update personal information and your avatar. Check the file requirements, save changes and read the result message. Appearance and language can be adjusted in settings.\n\n## Password\n\nChanging a password requires the current password, new password and confirmation. Follow the displayed password rules and check the result after submitting.\n\n“Start tour” opens the relevant page. Tours never save forms or change your account automatically."
87
105
  }