@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.
- package/README.en.md +25 -1
- package/README.ja.md +25 -1
- package/README.md +25 -1
- package/dist/{chunk-P6X63D3I.js → chunk-WW2MIO6T.js} +377 -124
- package/dist/chunk-WW2MIO6T.js.map +1 -0
- package/dist/{chunk-WWG3U5ZD.js → chunk-X3RSYNBG.js} +8 -3
- package/dist/chunk-X3RSYNBG.js.map +1 -0
- package/dist/cli-config-templates-QRF6B5T4.js +5 -0
- package/dist/{cli-config-templates-EFABFSCY.js.map → cli-config-templates-QRF6B5T4.js.map} +1 -1
- package/dist/{cli-skill-templates-2KDUACT6.js → cli-skill-templates-EZFW7D43.js} +3 -3
- package/dist/{cli-skill-templates-2KDUACT6.js.map → cli-skill-templates-EZFW7D43.js.map} +1 -1
- package/dist/index.js +31 -10
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
- package/templates/README.md +11 -4
- package/templates/apps/api/.gitignore.hbs +1 -0
- package/templates/apps/api/e2e/capture-evidence.ts.hbs +23 -0
- package/templates/apps/api/playwright.config.ts.hbs +1 -0
- package/templates/apps/fullstack/.gitignore.hbs +1 -0
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +123 -4
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +8 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_ai.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_audit.yml.hbs +1 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_cache.yml.hbs +1 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_core.yml.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_db.yml.hbs +1 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_deploy.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_iam.yml.hbs +1 -67
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_payment.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_reach.yml.hbs +31 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_scheduler.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_serv.yml.hbs +10 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_storage.yml.hbs +1 -3
- package/templates/apps/fullstack/packages/{{projectName}}-serv/config/_vecdb.yml.hbs +1 -0
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/app-server.ts.hbs +1 -21
- package/templates/apps/fullstack/packages/{{projectName}}-serv/src/server/procedures/admin-procedures.ts.hbs +1 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +7 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/en-US.json.hbs +19 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/messages/zh-CN.json.hbs +19 -1
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AdminWorkspace.svelte.hbs +7 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +5 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/ProfileSettings.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/stores/guide-store.ts.hbs +55 -0
- package/templates/apps/fullstack/playwright.config.ts.hbs +21 -10
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +35 -23
- package/templates/base/.gitignore.hbs +1 -2
- package/templates/base/e2e/app.spec.ts.hbs +49 -3
- package/templates/base/package.json.hbs +1 -1
- package/templates/skills/AGENTS.md +6 -0
- package/templates/skills/bridges/admin/AGENTS.md +6 -0
- package/templates/skills/bridges/api/AGENTS.md +6 -0
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +6 -0
- package/templates/skills/bridges/h5/AGENTS.md +6 -0
- package/templates/skills/bridges/mobile-app/AGENTS.md +6 -0
- package/templates/skills/bridges/website/AGENTS.md +6 -0
- package/templates/skills/copilot-instructions.md +3 -9
- package/templates/skills/hai-app-create/SKILL.md +6 -0
- package/templates/skills/hai-app-create/development.md +10 -6
- package/templates/skills/hai-app-create/reference.md +10 -0
- package/templates/skills/hai-audit/SKILL.md +1 -1
- package/templates/skills/hai-build/SKILL.md +1 -0
- package/templates/skills/hai-build/reference.md +1 -1
- package/templates/skills/hai-ci/SKILL.md +1 -1
- package/templates/skills/hai-framework-sync/SKILL.md +67 -70
- package/templates/skills/hai-framework-sync/sync.md +49 -0
- package/templates/skills/hai-iam/SKILL.md +5 -0
- package/templates/skills/hai-iam/reference.md +88 -7
- package/templates/skills/hai-pr-review/SKILL.md +1 -1
- package/templates/skills/hai-reach/SKILL.md +1 -1
- package/templates/skills/hai-reach/reference.md +4 -5
- package/templates/skills/hai-serv/SKILL.md +1 -1
- package/templates/skills/hai-serv/reference.md +9 -6
- package/templates/skills/hai-spec-implement/SKILL.md +41 -0
- package/templates/skills/hai-spec-implement/templates.md +90 -0
- package/templates/skills/hai-spec-implement/workflow.md +53 -0
- package/templates/skills/hai-spec-review/SKILL.md +44 -0
- package/templates/skills/hai-spec-review/checklist.md +70 -0
- package/templates/skills/hai-storage/SKILL.md +9 -2
- package/templates/skills/hai-ui/SKILL.md +3 -1
- package/templates/skills/hai-ui/guide.md +79 -0
- package/templates/skills/hai-ui/reference.md +13 -0
- package/templates/skills/opencode.json +1 -1
- package/dist/chunk-P6X63D3I.js.map +0 -1
- package/dist/chunk-WWG3U5ZD.js.map +0 -1
- package/dist/cli-config-templates-EFABFSCY.js +0 -5
- package/templates/skills/bridges/admin/copilot-instructions.md +0 -11
- package/templates/skills/bridges/api/copilot-instructions.md +0 -13
- package/templates/skills/bridges/fullstack/copilot-instructions.md.hbs +0 -19
- package/templates/skills/bridges/h5/copilot-instructions.md +0 -11
- package/templates/skills/bridges/mobile-app/copilot-instructions.md +0 -11
- 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.
|
|
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.
|
|
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.
|
|
45
|
-
"@h-ai/deploy": "0.1.0-alpha.
|
|
46
|
-
"@h-ai/reach": "0.1.0-alpha.
|
|
47
|
-
"@h-ai/reldb": "0.1.0-alpha.
|
|
48
|
-
"@h-ai/storage": "0.1.0-alpha.
|
|
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",
|
package/templates/README.md
CHANGED
|
@@ -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
|
|
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
|
-
- `
|
|
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
|
|
72
|
-
- `
|
|
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
|
|
@@ -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,
|
|
@@ -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
|
|
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
|
|
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
|
|
10
|
-
- `
|
|
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
|
-
-
|
|
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,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
type: memory
|
|
1
|
+
{{{moduleConfig 'cache' true}}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{{{moduleConfig 'deploy' features.deploy}}}
|
|
@@ -1,67 +1 @@
|
|
|
1
|
-
|
|
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]
|
|
@@ -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
|
|
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,
|
|
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
|
}
|