@h-ai/cli 1.0.0-beta.7 → 1.0.0-beta.9
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 +6 -6
- package/README.ja.md +6 -6
- package/README.md +6 -6
- package/dist/{chunk-XI5SKOOR.js → chunk-MEEFB4BI.js} +2 -2
- package/dist/{chunk-XI5SKOOR.js.map → chunk-MEEFB4BI.js.map} +1 -1
- package/dist/{chunk-RT5OZJKM.js → chunk-SGWSVWZ5.js} +3 -3
- package/dist/{chunk-RT5OZJKM.js.map → chunk-SGWSVWZ5.js.map} +1 -1
- package/dist/cli-config-templates-SVV3633D.js +5 -0
- package/dist/{cli-config-templates-5KS3AE4H.js.map → cli-config-templates-SVV3633D.js.map} +1 -1
- package/dist/index.js +64 -50
- package/dist/index.js.map +1 -1
- package/dist/metafile-esm.json +1 -1
- package/dist/{src-CX5IJGYM.js → src-IW4CY2IN.js} +3 -3
- package/dist/src-IW4CY2IN.js.map +1 -0
- package/package.json +16 -12
- package/templates/apps/api/apps/{{projectName}}-serv/config/_serv.yml.hbs +1 -1
- package/templates/apps/api/apps/{{projectName}}-serv/src/app.ts.hbs +6 -2
- package/templates/apps/api/apps/{{projectName}}-serv/tests/service.test.ts.hbs +29 -3
- package/templates/apps/fullstack/.gitignore.hbs +7 -2
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +2 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/package.json.hbs +1 -1
- package/templates/apps/mobile-app/messages/en-US.json +1 -1
- package/templates/apps/mobile-app/messages/ja-JP.json +1 -1
- package/templates/apps/mobile-app/messages/zh-CN.json +1 -1
- package/templates/apps/mobile-app/src/lib/api.ts.hbs +8 -5
- package/templates/base/.gitignore.hbs +7 -2
- package/templates/base/e2e/app.spec.ts.hbs +9 -4
- package/templates/base/package.json.hbs +2 -2
- package/templates/skills/hai-ai/acp.md +2 -0
- package/templates/skills/hai-ai/reference.md +1 -1
- package/templates/skills/hai-api-client/reference.md +2 -0
- package/templates/skills/hai-cache/reference.md +6 -2
- package/templates/skills/hai-core/SKILL.md +1 -1
- package/templates/skills/hai-crypto/reference.md +2 -0
- package/templates/skills/hai-payment/reference.md +39 -17
- package/templates/skills/hai-reach/reference.md +15 -7
- package/templates/skills/hai-reldb/reference.md +5 -2
- package/templates/skills/hai-scheduler/reference.md +1 -1
- package/templates/skills/hai-storage/reference.md +8 -2
- package/templates/skills/hai-ui/SKILL.md +3 -1
- package/templates/skills/hai-ui/charts.md +51 -0
- package/templates/skills/hai-vecdb/reference.md +7 -2
- package/dist/cli-config-templates-5KS3AE4H.js +0 -5
- package/dist/src-CX5IJGYM.js.map +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, canKeepMiddleware, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, parseMiddlewareField, readDeploymentArtifact, resolveDownloadProxy, resolveImageReference, saveDeploymentEnvironment, saveMiddlewareEnvironment, validateMiddlewareField } from './chunk-
|
|
2
|
+
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, canKeepMiddleware, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, importDeploymentSecrets, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, parseMiddlewareField, readDeploymentArtifact, resolveDownloadProxy, resolveImageReference, saveDeploymentEnvironment, saveMiddlewareEnvironment, validateMiddlewareField } from './chunk-MEEFB4BI.js';
|
|
3
3
|
import './chunk-ZSDEVXZ7.js';
|
|
4
|
-
//# sourceMappingURL=src-
|
|
5
|
-
//# sourceMappingURL=src-
|
|
4
|
+
//# sourceMappingURL=src-IW4CY2IN.js.map
|
|
5
|
+
//# sourceMappingURL=src-IW4CY2IN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"src-IW4CY2IN.js"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@h-ai/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.9",
|
|
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.23.4 || ^24.22.1 || ^26.12.1"
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@h-ai/core": "1.0.0-beta.
|
|
33
|
+
"@h-ai/core": "1.0.0-beta.9",
|
|
34
34
|
"cac": "^7.0.1",
|
|
35
35
|
"chalk": "^6.0.1",
|
|
36
36
|
"fast-wrap-ansi": "^0.2.2",
|
|
@@ -41,15 +41,18 @@
|
|
|
41
41
|
"yaml": "^2.9.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@h-ai/api-client": "1.0.0-beta.
|
|
45
|
-
"@h-ai/cache": "1.0.0-beta.
|
|
46
|
-
"@h-ai/crypto": "1.0.0-beta.
|
|
47
|
-
"@h-ai/deploy": "1.0.0-beta.
|
|
48
|
-
"@h-ai/feedback": "1.0.0-beta.
|
|
49
|
-
"@h-ai/iam": "1.0.0-beta.
|
|
50
|
-
"@h-ai/
|
|
51
|
-
"@h-ai/
|
|
52
|
-
"@h-ai/
|
|
44
|
+
"@h-ai/api-client": "1.0.0-beta.9",
|
|
45
|
+
"@h-ai/cache": "1.0.0-beta.9",
|
|
46
|
+
"@h-ai/crypto": "1.0.0-beta.9",
|
|
47
|
+
"@h-ai/deploy": "1.0.0-beta.9",
|
|
48
|
+
"@h-ai/feedback": "1.0.0-beta.9",
|
|
49
|
+
"@h-ai/iam": "1.0.0-beta.9",
|
|
50
|
+
"@h-ai/kit": "1.0.0-beta.9",
|
|
51
|
+
"@h-ai/payment": "1.0.0-beta.9",
|
|
52
|
+
"@h-ai/reach": "1.0.0-beta.9",
|
|
53
|
+
"@h-ai/reldb": "1.0.0-beta.9",
|
|
54
|
+
"@h-ai/storage": "1.0.0-beta.9",
|
|
55
|
+
"@h-ai/vecdb": "1.0.0-beta.9",
|
|
53
56
|
"@types/fs-extra": "^11.0.4",
|
|
54
57
|
"@types/node": "^26.6.5",
|
|
55
58
|
"@types/prompts": "^2.4.9",
|
|
@@ -59,7 +62,8 @@
|
|
|
59
62
|
"testcontainers": "^12.2.0",
|
|
60
63
|
"tsup": "^8.5.1",
|
|
61
64
|
"typescript": "^6.0.3",
|
|
62
|
-
"vitest": "^4.1.11"
|
|
65
|
+
"vitest": "^4.1.11",
|
|
66
|
+
"zod": "^4.6.5"
|
|
63
67
|
},
|
|
64
68
|
"scripts": {
|
|
65
69
|
"build": "tsup",
|
|
@@ -33,7 +33,7 @@ http:
|
|
|
33
33
|
|
|
34
34
|
cors:
|
|
35
35
|
origin: ${HAI_SERV_CORS_ORIGIN:*}
|
|
36
|
-
nativeOrigins: ${HAI_SERV_CORS_NATIVEORIGINS:http://localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
|
|
36
|
+
nativeOrigins: ${HAI_SERV_CORS_NATIVEORIGINS:http://localhost,https://localhost,http://tauri.localhost,https://tauri.localhost,tauri://localhost,capacitor://localhost}
|
|
37
37
|
{{#if features.iam}} credentials: ${HAI_SERV_CORS_CREDENTIALS:true}
|
|
38
38
|
{{else}} credentials: ${HAI_SERV_CORS_CREDENTIALS:false}
|
|
39
39
|
{{/if}} allowedHeaders:
|
|
@@ -68,7 +68,11 @@ export function {{api.serviceAppFactoryName}}(_options: CreateApiServiceAppOptio
|
|
|
68
68
|
{{else}} const transport = undefined
|
|
69
69
|
{{/if}}
|
|
70
70
|
{{#if features.iam}}
|
|
71
|
-
const refreshCookie = _options.refreshCookie === 'disabled'
|
|
71
|
+
const refreshCookie = _options.refreshCookie === 'disabled'
|
|
72
|
+
? undefined
|
|
73
|
+
: { secure: security.secureRefreshCookie, nativeTokenTransport: { isRequest: (request: Request) => request.headers.get('x-hai-token-transport') === 'body'
|
|
74
|
+
&& ['hai-mobile-app', 'hai-desktop-app'].includes(request.headers.get('x-request-client') ?? '')
|
|
75
|
+
&& security.isNativeOrigin(request.headers.get('origin') ?? '') } }
|
|
72
76
|
{{/if}}
|
|
73
77
|
{{#if features.feedback}}
|
|
74
78
|
// 普通业务日志与反馈使用同一可信应用范围;身份仍由 IAM 验证。
|
|
@@ -113,7 +117,7 @@ export function {{api.serviceAppFactoryName}}(_options: CreateApiServiceAppOptio
|
|
|
113
117
|
middleware: serv.cors({
|
|
114
118
|
origin: security.allowOrigin,
|
|
115
119
|
credentials: servConfig.cors.credentials,
|
|
116
|
-
allowedHeaders: [...servConfig.cors.allowedHeaders],
|
|
120
|
+
allowedHeaders: [...new Set([...servConfig.cors.allowedHeaders, 'x-request-client', 'x-hai-token-transport'])],
|
|
117
121
|
exposedHeaders: [...servConfig.cors.exposedHeaders],
|
|
118
122
|
}),
|
|
119
123
|
},
|
|
@@ -14,7 +14,7 @@ import { join } from 'node:path'
|
|
|
14
14
|
{{#if features.storage}}import { storage } from '@h-ai/storage'
|
|
15
15
|
{{/if}}{{#if features.vecdb}}import { vecdb } from '@h-ai/vecdb'
|
|
16
16
|
{{/if}}import { {{api.contractExportName}} } from '{{api.contractPackageName}}'
|
|
17
|
-
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
|
17
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
|
18
18
|
|
|
19
19
|
import { {{api.serviceAppFactoryName}} } from '../src/app.js'
|
|
20
20
|
|
|
@@ -44,6 +44,7 @@ beforeAll(async () => {
|
|
|
44
44
|
{{/if}}{{#if features.vecdb}} vecdbRoot = join(tempRoot, 'vecdb')
|
|
45
45
|
{{/if}}
|
|
46
46
|
|
|
47
|
+
vi.stubEnv('HAI_SERV_CORS_NATIVEORIGINS', undefined)
|
|
47
48
|
core.init({
|
|
48
49
|
configDir: './config',
|
|
49
50
|
logging: { level: 'warn' },
|
|
@@ -82,8 +83,7 @@ beforeAll(async () => {
|
|
|
82
83
|
{{/if}}
|
|
83
84
|
|
|
84
85
|
const app = {{api.serviceAppFactoryName}}({
|
|
85
|
-
transport: 'disabled',
|
|
86
|
-
refreshCookie: 'disabled',{{/if}}
|
|
86
|
+
transport: 'disabled',
|
|
87
87
|
})
|
|
88
88
|
const listening = new Promise<AddressInfo>((resolve) => {
|
|
89
89
|
server = serv.listen(app, {
|
|
@@ -98,6 +98,8 @@ beforeAll(async () => {
|
|
|
98
98
|
client = apiClient.create({{api.contractExportName}})
|
|
99
99
|
const initResult = await client.init({
|
|
100
100
|
baseUrl: `${baseUrl}${servConfig.http.apiPrefix}`,
|
|
101
|
+
clientName: 'hai-mobile-app',
|
|
102
|
+
headers: { 'origin': 'capacitor://localhost', 'x-hai-token-transport': 'body' },
|
|
101
103
|
auth: { storage: apiClient.tokenStorage.memory() },
|
|
102
104
|
})
|
|
103
105
|
if (!initResult.success)
|
|
@@ -114,6 +116,7 @@ afterAll(async () => {
|
|
|
114
116
|
{{/if}}{{#if features.cache}} await cache.close()
|
|
115
117
|
{{/if}}{{#if features.db}} await reldb.close()
|
|
116
118
|
{{/if}} rmSync(tempRoot, { recursive: true, force: true })
|
|
119
|
+
vi.unstubAllEnvs()
|
|
117
120
|
})
|
|
118
121
|
|
|
119
122
|
describe('{{api.servPackageName}}', () => {
|
|
@@ -144,6 +147,29 @@ describe('{{api.servPackageName}}', () => {
|
|
|
144
147
|
})
|
|
145
148
|
|
|
146
149
|
{{#if features.iam}}
|
|
150
|
+
it.each([
|
|
151
|
+
['capacitor://localhost', 'hai-mobile-app'],
|
|
152
|
+
['https://localhost', 'hai-mobile-app'],
|
|
153
|
+
['http://tauri.localhost', 'hai-desktop-app'],
|
|
154
|
+
['https://tauri.localhost', 'hai-desktop-app'],
|
|
155
|
+
['tauri://localhost', 'hai-desktop-app'],
|
|
156
|
+
])('refreshes an expired access token at the default native Origin %s', async (origin, clientName) => {
|
|
157
|
+
const native = apiClient.create({{api.contractExportName}})
|
|
158
|
+
try {
|
|
159
|
+
expect((await native.init({ baseUrl: `${baseUrl}${servConfig.http.apiPrefix}`, clientName, headers: { 'origin': origin, 'x-hai-token-transport': 'body' }, auth: { storage: apiClient.tokenStorage.memory() } })).success).toBe(true)
|
|
160
|
+
const registration = await apiClient.createAuthActions(native).register(uniqueUser('refresh'))
|
|
161
|
+
expect(registration.success).toBe(true)
|
|
162
|
+
if (!registration.success)
|
|
163
|
+
return
|
|
164
|
+
expect(registration.data.tokens.refreshToken).toBeTruthy()
|
|
165
|
+
await native.auth.setTokens({ ...registration.data.tokens, accessToken: 'expired-access-for-test' })
|
|
166
|
+
expect((await native.iam.auth.currentUser()).success).toBe(true)
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
await native.close()
|
|
170
|
+
}
|
|
171
|
+
})
|
|
172
|
+
|
|
147
173
|
it('registers a user and returns authenticated echo context', async () => {
|
|
148
174
|
const user = uniqueUser('svc')
|
|
149
175
|
const authActions = apiClient.createAuthActions(client)
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"lint:fix": "eslint . --fix",
|
|
24
24
|
"test": "pnpm paraglide:compile && vitest run --passWithNoTests",
|
|
25
25
|
"test:watch": "pnpm paraglide:compile && vitest",
|
|
26
|
-
"clean": "rimraf dist build
|
|
26
|
+
"clean": "rimraf dist build .turbo src/lib/paraglide",
|
|
27
27
|
"cap:add:android": "cap add android",
|
|
28
28
|
"cap:add:ios": "cap add ios",
|
|
29
29
|
"cap:sync": "pnpm build && cap sync",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"status_runtime_web": "Browser preview",
|
|
25
25
|
"status_token_label": "Token storage",
|
|
26
26
|
"status_token_native": "Secure storage",
|
|
27
|
-
"status_token_memory": "Memory
|
|
27
|
+
"status_token_memory": "Memory access token + httpOnly cookie",
|
|
28
28
|
"tasks_title": "Today's tasks",
|
|
29
29
|
"tasks_sync": "Wire backend APIs",
|
|
30
30
|
"tasks_polish": "Polish mobile UX",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"status_runtime_web": "ブラウザープレビュー",
|
|
25
25
|
"status_token_label": "トークン保存",
|
|
26
26
|
"status_token_native": "安全なストレージ",
|
|
27
|
-
"status_token_memory": "
|
|
27
|
+
"status_token_memory": "メモリー内access token + httpOnly Cookie",
|
|
28
28
|
"tasks_title": "本日のタスク",
|
|
29
29
|
"tasks_sync": "バックエンドAPIを接続",
|
|
30
30
|
"tasks_polish": "モバイルUXを改善",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"status_runtime_web": "浏览器预览",
|
|
25
25
|
"status_token_label": "Token 存储",
|
|
26
26
|
"status_token_native": "安全存储",
|
|
27
|
-
"status_token_memory": "
|
|
27
|
+
"status_token_memory": "内存 access token + httpOnly Cookie",
|
|
28
28
|
"tasks_title": "今日任务",
|
|
29
29
|
"tasks_sync": "同步后端接口",
|
|
30
30
|
"tasks_polish": "打磨移动端体验",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* {{projectName}} — API 客户端初始化。
|
|
3
|
-
* Web
|
|
3
|
+
* Web 预览使用内存 access token + httpOnly refresh Cookie;原生端使用 Capacitor 安全存储。
|
|
4
4
|
* 原生存储失败会拒绝 Promise;登录/退出调用方须处理 auth.setTokens/clear 失败并释放界面 loading。
|
|
5
5
|
*/
|
|
6
6
|
import { apiClient } from '@h-ai/api-client'
|
|
@@ -56,7 +56,7 @@ export function resolveApiBase(): string {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
|
-
* 查询实际 Capacitor 平台,决定安全原生存储或 Web
|
|
59
|
+
* 查询实际 Capacitor 平台,决定安全原生存储或 Web Cookie 刷新通道。
|
|
60
60
|
* @returns 是否处于 Capacitor 原生平台;浏览器预览返回 false。
|
|
61
61
|
* @throws Error - 平台模块加载或检测失败时拒绝 Promise,不伪装为 Web 模式。
|
|
62
62
|
* @example 模块内部上下文
|
|
@@ -74,7 +74,7 @@ export async function usesNativeTokenStorage(): Promise<boolean> {
|
|
|
74
74
|
|
|
75
75
|
/**
|
|
76
76
|
* 按真实平台创建 TokenStorage;绑定 client 后通过 client.auth 变更身份。
|
|
77
|
-
* @returns
|
|
77
|
+
* @returns 原生安全存储或内存 access token + httpOnly refresh Cookie;其方法的读写故障继续拒绝 Promise。
|
|
78
78
|
* @throws Error - 平台检测或存储工厂失败时拒绝,不改用较弱的持久化方式。
|
|
79
79
|
* @example 模块内部上下文
|
|
80
80
|
* ```ts
|
|
@@ -86,7 +86,7 @@ async function createTokenStorage() {
|
|
|
86
86
|
if (await usesNativeTokenStorage())
|
|
87
87
|
return createCapacitorTokenStorage()
|
|
88
88
|
|
|
89
|
-
return apiClient.tokenStorage.
|
|
89
|
+
return apiClient.tokenStorage.httpOnlyCookie()
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
@@ -137,6 +137,7 @@ export async function initApi(): Promise<void> {
|
|
|
137
137
|
* ```
|
|
138
138
|
*/
|
|
139
139
|
async function initialize(): Promise<void> {
|
|
140
|
+
const native = await usesNativeTokenStorage()
|
|
140
141
|
const storage = await createTokenStorage()
|
|
141
142
|
{{#if features.crypto}}
|
|
142
143
|
const transport = resolveTransport()
|
|
@@ -149,6 +150,8 @@ async function initialize(): Promise<void> {
|
|
|
149
150
|
}
|
|
150
151
|
const result = await apiClient.init({
|
|
151
152
|
baseUrl: resolveApiBase(),
|
|
153
|
+
clientName: 'hai-mobile-app',
|
|
154
|
+
headers: native ? { 'x-hai-token-transport': 'body' } : {},
|
|
152
155
|
auth: { storage },
|
|
153
156
|
...(transport ? { transport } : {}),
|
|
154
157
|
})
|
|
@@ -169,7 +172,7 @@ async function initialize(): Promise<void> {
|
|
|
169
172
|
throw error
|
|
170
173
|
}
|
|
171
174
|
{{else}}
|
|
172
|
-
const result = await apiClient.init({ baseUrl: resolveApiBase(), auth: { storage } })
|
|
175
|
+
const result = await apiClient.init({ baseUrl: resolveApiBase(), clientName: 'hai-mobile-app', headers: native ? { 'x-hai-token-transport': 'body' } : {}, auth: { storage } })
|
|
173
176
|
if (!result.success)
|
|
174
177
|
throw new Error(result.error.message)
|
|
175
178
|
initialized = true
|
|
@@ -120,12 +120,16 @@ test('configured IAM registration and password login', async ({ page, context })
|
|
|
120
120
|
await registerForm.locator('#register-confirm-password').fill(password)
|
|
121
121
|
await registerForm.locator('button[type="submit"]').click()
|
|
122
122
|
await expect(page.locator('.register-form')).not.toBeVisible()
|
|
123
|
-
{{#
|
|
123
|
+
{{#if isCapacitorApp}}
|
|
124
|
+
// 浏览器预览通过刷新 Cookie 恢复会话,不能把页面重载当作退出。
|
|
125
|
+
await page.reload()
|
|
126
|
+
await expect(page.getByRole('navigation')).toBeVisible()
|
|
127
|
+
{{else}}
|
|
124
128
|
// 通过页面 fetch 复用 Cookie 与已安装的传输加密,避免 HTTP 辅助客户端绕过浏览器协议。
|
|
125
129
|
const me = await page.evaluate(async url => (await fetch(url)).status, new URL('./api/auth/me', test.info().project.use.baseURL).href)
|
|
126
130
|
expect(me).toBe(200)
|
|
131
|
+
{{/if}}
|
|
127
132
|
await context.clearCookies()
|
|
128
|
-
{{/unless}}
|
|
129
133
|
await page.reload()
|
|
130
134
|
await page.goto(authUrl)
|
|
131
135
|
await expect(page.locator('#login-username')).toBeVisible()
|
|
@@ -204,8 +208,9 @@ test('configured IAM registration and password login', async ({ page, context })
|
|
|
204
208
|
expect(new URL(page.url()).searchParams.has('token')).toBe(false)
|
|
205
209
|
await page.reload()
|
|
206
210
|
{{#if isCapacitorApp}}
|
|
207
|
-
//
|
|
208
|
-
await expect(page.
|
|
211
|
+
// 已消费的重置令牌不再参与启动;刷新 Cookie 恢复同一已登录会话。
|
|
212
|
+
await expect(page.getByRole('navigation')).toBeVisible()
|
|
213
|
+
await expect(page.locator('#login-username')).not.toBeVisible()
|
|
209
214
|
{{else}}
|
|
210
215
|
expect(await page.evaluate(async url => (await fetch(url)).status, new URL('./api/auth/me', test.info().project.use.baseURL).href)).toBe(200)
|
|
211
216
|
{{/if}}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"engines": { "node": "^22.23.4 || ^24.22.1 || ^26.12.1" },
|
|
3
3
|
"name": "{{projectName}}",
|
|
4
|
-
"version": "1.0.0-beta.
|
|
4
|
+
"version": "1.0.0-beta.9",
|
|
5
5
|
"private": true,
|
|
6
6
|
"packageManager": "pnpm@12.11.2",
|
|
7
7
|
"description": "{{#if_eq appType 'admin'}}Enterprise management system built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'website'}}Enterprise website built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'h5'}}Mobile H5 app built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'api'}}REST API service built with hai Agent Framework{{/if_eq}}{{#if_eq appType 'mobile-app'}}Mobile app built with hai Agent Framework{{/if_eq}}",
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"test": "{{#if hasI18n}}pnpm paraglide:compile && {{/if}}vitest run",
|
|
42
42
|
"test:watch": "vitest",
|
|
43
43
|
"test:e2e": "node scripts/run-e2e.mjs",
|
|
44
|
-
"clean": "{{#if isSvelteOnlyApp}}rimraf dist build
|
|
44
|
+
"clean": "{{#if isSvelteOnlyApp}}rimraf dist build .turbo src/lib/paraglide{{else}}rimraf .svelte-kit build .turbo{{/if}}"{{#if isCapacitorApp}},
|
|
45
45
|
"cap:add:android": "cap add android",
|
|
46
46
|
"cap:add:ios": "cap add ios",
|
|
47
47
|
"cap:sync": "pnpm compile && cap sync",
|
|
@@ -14,6 +14,8 @@ Codex 用 `adapter: 'codex'`,DeepSeek Harness 用 `adapter: 'deepseek-harness'
|
|
|
14
14
|
|
|
15
15
|
内置 Codex/dsh 通过 stdio 使用。HTTP/WS 是锁定 SDK 2.2.0 的官方实验传输,服务必须实现相同的 ACP 传输版本;不把任意 HTTP 聊天接口当 ACP,也不自动架设 stdio 网关。自定义服务必须声明 `supportedTransports`,协议不匹配返回错误。
|
|
16
16
|
|
|
17
|
+
Linux 上运行 embedded Codex 前,按 [Codex 沙箱前置](https://learn.chatgpt.com/docs/sandboxing) 准备 bubblewrap 和所需的 AppArmor profile,并用 `codex sandbox` 在临时目录验证读写。沙箱初始化失败应先修复宿主环境,不能通过关闭沙箱或让模型猜测文件内容完成任务。
|
|
18
|
+
|
|
17
19
|
## 原生 HTTP/WS 服务:Goose
|
|
18
20
|
|
|
19
21
|
Goose 1.57.0 自身提供 `goose serve` 与 `/acp` HTTP/WS,使用 custom-service 接入。先由部署方启动服务、挂载工作区/状态卷,并在 Goose 端维护模型(例如 `GOOSE_PROVIDER=openrouter`、`GOOSE_MODEL`、`OPENROUTER_API_KEY`);服务独立设置 `GOOSE_SERVER__SECRET_KEY`。Hai 只使用 `X-Secret-Key` 认证,不向服务注入 Hai 模型密钥。
|
|
@@ -193,7 +193,7 @@ flush 对本节点后台提取的异常和失败 HaiResult 返回 MEMORY_EXTRACT
|
|
|
193
193
|
|
|
194
194
|
- `ai.file.parse({ content, filename, options })`:text/html/pdf/docx/ocr 解析。
|
|
195
195
|
- `ai.file.parseText(content, filename?)`:直接返回文本。
|
|
196
|
-
- `ai.rerank.rerank(request)` / `rerankTexts(query, texts, topN?)
|
|
196
|
+
- `ai.rerank.rerank(request)` / `rerankTexts(query, texts, topN?)`:相关性重排;模型 `timeout` 覆盖响应头与正文读取,超时返回失败结果。
|
|
197
197
|
- `ai.reasoning.run(query, options?)` / `runStream(query, options?)`:`react`、`cot`、`plan-execute`。
|
|
198
198
|
|
|
199
199
|
## Audio
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
## 使用步骤
|
|
11
11
|
|
|
12
|
+
`client.close()` 使旧刷新失效并等待已开始的凭据写入,但不清除宿主 TokenStorage。关闭完成后可复用同一存储重新 init;迟到刷新不能覆盖新身份。请求 timeout 覆盖响应头与完整 JSON 正文;自定义流按需读取,以调用方 signal 取消正文。内置 Cookie 适配器的登录和刷新包含跨域凭据,服务端须配置明确 Origin;内存/原生存储保留原请求凭据策略。
|
|
13
|
+
|
|
12
14
|
### 1. 初始化
|
|
13
15
|
|
|
14
16
|
浏览器默认使用 httpOnly cookie 存储(推荐):
|
|
@@ -38,9 +38,13 @@ type: memory # memory | redis
|
|
|
38
38
|
### 2. 初始化与关闭
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
import {
|
|
41
|
+
import { core } from '@h-ai/core'
|
|
42
|
+
import { cache, CacheConfigSchema } from '@h-ai/cache'
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
const config = core.config.validate('cache', CacheConfigSchema)
|
|
45
|
+
if (!config.success) throw new Error(config.error.message)
|
|
46
|
+
const initialized = await cache.init(config.data)
|
|
47
|
+
if (!initialized.success) throw new Error(initialized.error.message)
|
|
44
48
|
// 使用后关闭
|
|
45
49
|
const closed = await cache.close()
|
|
46
50
|
if (!closed.success) throw new Error(closed.error.message)
|
|
@@ -21,7 +21,7 @@ description: "配置 hai 应用启动、YAML/环境变量、日志、i18n 与 Ha
|
|
|
21
21
|
- 自动加载和监听配置目录内所有 YAML;项目根 `hai.json` 的 `build` / `deploy` 由 CLI 读取,不进入运行时配置。单项 load 返回失败时记日志并继续,必需配置须 validate 并检查 success;目录扫描、日志设置等异常仍可能传播。
|
|
22
22
|
- 应用名称、版本、环境、默认语言和日志设置统一从 `_core.yml` 经 core.config 获取,日志使用 core.logger;UI 不另维护硬编码副本。SvelteKit layout / fullstack app.info 仅下发公开字段白名单;独立 Vite mobile-app 在 Node 构建时解析并注入公开默认值,修改这些编译默认值需要重新构建。静态 Web 的公开 API 地址等通过 `runtime-config.js` 覆盖时无需重建镜像,原生包在复制静态资源前准备配置入口。浏览器不加载 YAML,不接收完整模块配置、密钥或日志脱敏上下文。
|
|
23
23
|
- `HAI_<配置名>_<路径>` 支持父 JSON 基础与细项覆盖,同源先父后子,高来源整段替换清除低来源细项。camelCase 不拆词,约定变量优先于显式插值;宿主显式提供 `configSchemas` 识别 YAML 缺少的可选路径,模块执行业务校验。
|
|
24
|
-
-
|
|
24
|
+
- 环境文件约定:`.env.example` 完整列出字段用途、默认值、枚举和范围;`.env` 为公共基础;`.env.dev`、`.env.test`、`.env.prod` 为公共环境覆盖,公共文件提交 Git。`.env.local` 为共享私有配置,`.env.<mode>.local` 为模式私有配置,均不提交 Git。优先级为进程环境 > `.env.<mode>.local` > `.env.local` > `.env.<mode>` > `.env` > YAML;Vite 在 Node 配置阶段调用 `core.config.loadEnvFiles({ mode, envDir })` 并检查 success 后注入进程,Node 入口复用 `run-node.mjs` 的结构合并,避免原生多 `--env-file` 遗留低来源细项。`core.init` 不自行读取 dotenv。
|
|
25
25
|
- 配置 getter 与 ID/logger 方法不是 HaiResult;validate/load 等须检查 success。
|
|
26
26
|
- 日志可用 `{ error }` 记录原生 Error;两端先规范化诊断再字段/URL 脱敏,配置 redact 路径仍生效。默认包含 passwordHash、passwd、secretKey 等实际凭据字段;异常文本清理显式凭据标签和 URL,不保证识别无标签秘密。原异常不变,错误子图循环/深度用固定标记;业务隐私仍由调用方清理或遮蔽整条路径。
|
|
27
27
|
- 初始化前创建的默认、模块与嵌套 child Logger 在后续输出中应用当前全局 logging;configure/setLevel 也作用于已有实例。create 的显式 level/format/context 优先,未指定项动态继承。浏览器 json/pretty 与附加 redact 路径实际生效,消息文本仍由调用方脱敏。
|
|
@@ -244,30 +244,52 @@ import { apiContract } from '@h-ai/api-contract'
|
|
|
244
244
|
|
|
245
245
|
### 服务端路由(SvelteKit)
|
|
246
246
|
|
|
247
|
+
应用实现 `readOrder`:从共享数据库读取订单,返回持久化的金额、币种、所有者与支付状态。通知地址取服务端配置;在宿主权限配置中登记并授予 `payment:create`。以下工厂在路由模块中注入这两个可信依赖,`kit.handler` 仅提供错误边界,认证和资源授权仍在示例中显式执行。
|
|
248
|
+
|
|
247
249
|
```typescript
|
|
250
|
+
import type { HaiResult } from '@h-ai/core'
|
|
248
251
|
import { kit } from '@h-ai/kit'
|
|
249
|
-
// src/routes/api/v1/payment/create/+server.ts
|
|
250
252
|
import { payment } from '@h-ai/payment'
|
|
253
|
+
import { z } from 'zod'
|
|
254
|
+
|
|
255
|
+
interface PayableOrder {
|
|
256
|
+
orderNo: string
|
|
257
|
+
ownerId: string
|
|
258
|
+
amount: number
|
|
259
|
+
currency: string
|
|
260
|
+
description: string
|
|
261
|
+
status: 'pending' | 'paid' | 'closed'
|
|
262
|
+
}
|
|
251
263
|
|
|
252
|
-
export
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
264
|
+
export function createPaymentHandler(
|
|
265
|
+
readOrder: (id: string) => Promise<HaiResult<PayableOrder | null>>,
|
|
266
|
+
notifyUrl: string,
|
|
267
|
+
) {
|
|
268
|
+
return kit.handler(async ({ request, locals }) => {
|
|
269
|
+
const session = locals.session
|
|
270
|
+
if (!session) return kit.response.unauthorized()
|
|
271
|
+
kit.guard.require(session, 'payment:create')
|
|
272
|
+
const input = await kit.validate.body(request, z.object({ orderId: z.string().min(1) }).strict())
|
|
273
|
+
const order = await readOrder(input.orderId)
|
|
274
|
+
if (!order.success) return kit.response.fromError(order.error)
|
|
275
|
+
if (!order.data || order.data.ownerId !== session.userId || order.data.status !== 'pending')
|
|
276
|
+
return kit.response.forbidden()
|
|
277
|
+
const result = await payment.createOrder('stripe', {
|
|
278
|
+
orderNo: order.data.orderNo,
|
|
279
|
+
amount: order.data.amount,
|
|
280
|
+
currency: order.data.currency,
|
|
281
|
+
description: order.data.description,
|
|
282
|
+
tradeType: 'h5',
|
|
283
|
+
notifyUrl,
|
|
284
|
+
})
|
|
285
|
+
if (!result.success) return kit.response.fromError(result.error, locals.requestId)
|
|
286
|
+
return kit.response.ok(result.data, locals.requestId)
|
|
261
287
|
})
|
|
262
|
-
|
|
263
|
-
if (!result.success) {
|
|
264
|
-
return kit.response.internalError(result.error.message)
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
return kit.response.ok(result.data, locals.requestId)
|
|
268
|
-
})
|
|
288
|
+
}
|
|
269
289
|
```
|
|
270
290
|
|
|
291
|
+
在 `+server.ts` 中将可信业务读取函数和通知地址传给工厂,再导出返回值为 `POST`。
|
|
292
|
+
|
|
271
293
|
### 完整支付流程
|
|
272
294
|
|
|
273
295
|
```
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
## 运行环境
|
|
8
8
|
|
|
9
|
+
Provider 名称必须唯一,重名在连接前返回 CONFIG_ERROR。close 等待在途初始化并释放其 Provider,返回后不再迟到发布模块或定时器。
|
|
10
|
+
|
|
9
11
|
> ⚠️ **服务端模块(Node.js only)。** 浏览器端不直接发送邮件/短信,而是通过 API 端点触发服务端 `reach.send()`。
|
|
10
12
|
|
|
11
13
|
## 依赖
|
|
@@ -76,21 +78,27 @@ API Provider 只接受 HTTP(S) URL。回调地址若来自不可信输入,业
|
|
|
76
78
|
### 2. 初始化与关闭
|
|
77
79
|
|
|
78
80
|
```typescript
|
|
79
|
-
import { cache } from '@h-ai/cache'
|
|
81
|
+
import { cache, CacheConfigSchema } from '@h-ai/cache'
|
|
80
82
|
import { core } from '@h-ai/core'
|
|
81
|
-
import { reach } from '@h-ai/reach'
|
|
82
|
-
import { reldb } from '@h-ai/reldb'
|
|
83
|
+
import { reach, ReachConfigSchema } from '@h-ai/reach'
|
|
84
|
+
import { reldb, ReldbConfigSchema } from '@h-ai/reldb'
|
|
83
85
|
|
|
84
86
|
// 当前配置启用了 delay:数据库和队列表为必需依赖。
|
|
85
|
-
const
|
|
87
|
+
const databaseConfig = core.config.validate('db', ReldbConfigSchema)
|
|
88
|
+
if (!databaseConfig.success) throw new Error(databaseConfig.error.message)
|
|
89
|
+
const database = await reldb.init(databaseConfig.data)
|
|
86
90
|
if (!database.success)
|
|
87
91
|
throw new Error(database.error.message)
|
|
88
|
-
const
|
|
92
|
+
const cacheConfig = core.config.validate('cache', CacheConfigSchema)
|
|
93
|
+
if (!cacheConfig.success) throw new Error(cacheConfig.error.message)
|
|
94
|
+
const cached = await cache.init(cacheConfig.data) // 可选,启用 flush 分布式锁
|
|
89
95
|
if (!cached.success)
|
|
90
96
|
throw new Error(cached.error.message)
|
|
91
97
|
|
|
92
98
|
// 再初始化触达模块(自动检测已初始化的 reldb/cache 单例)
|
|
93
|
-
const
|
|
99
|
+
const reachConfig = core.config.validate('reach', ReachConfigSchema)
|
|
100
|
+
if (!reachConfig.success) throw new Error(reachConfig.error.message)
|
|
101
|
+
const initialized = await reach.init(reachConfig.data)
|
|
94
102
|
if (!initialized.success)
|
|
95
103
|
throw new Error(initialized.error.message)
|
|
96
104
|
// ... 使用触达服务
|
|
@@ -234,7 +242,7 @@ interface ReachConfigInput {
|
|
|
234
242
|
|
|
235
243
|
验证码和密码重置等有时效的通知应明确选择 DND 策略;可靠入队成功不代表用户已收到验证码。
|
|
236
244
|
|
|
237
|
-
队列 `created_at` / `updated_at` / `locked_until` / `next_attempt_at` 均为 Unix 毫秒 `BIGINT`(SQLite 为 64 位 INTEGER);仓库把 driver 的字符串/数字归一化为安全整数 number,并拒绝负数、小数和不安全整数。模板仓库的 TIMESTAMP 时间返回 Date
|
|
245
|
+
队列 `created_at` / `updated_at` / `locked_until` / `next_attempt_at` 均为 Unix 毫秒 `BIGINT`(SQLite 为 64 位 INTEGER);仓库把 driver 的字符串/数字归一化为安全整数 number,并拒绝负数、小数和不安全整数。模板仓库的 TIMESTAMP 时间返回 Date。初始化自动执行版本化迁移,包含次数、退避策略和失败分类;升级前先 dry-run、备份并停止旧写入。旧通知载荷退役会清空正文/变量并将旧待发项标记 discarded,回退镜像不能恢复这些数据;跨库验收必须运行 PostgreSQL/MySQL 用例,SQLite 无法证明 32 位溢出已经消除。
|
|
238
246
|
|
|
239
247
|
### 租约、接收端幂等与未知结果
|
|
240
248
|
|
|
@@ -62,9 +62,12 @@ PostgreSQL 空闲连接错误已监听,由池移除并在下一次查询替换
|
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
64
|
import { core } from '@h-ai/core'
|
|
65
|
-
import { reldb } from '@h-ai/reldb'
|
|
65
|
+
import { reldb, ReldbConfigSchema } from '@h-ai/reldb'
|
|
66
66
|
|
|
67
|
-
|
|
67
|
+
const config = core.config.validate('db', ReldbConfigSchema)
|
|
68
|
+
if (!config.success) throw new Error(config.error.message)
|
|
69
|
+
const initialized = await reldb.init(config.data)
|
|
70
|
+
if (!initialized.success) throw new Error(initialized.error.message)
|
|
68
71
|
// ... 使用数据库
|
|
69
72
|
const closed = await reldb.close()
|
|
70
73
|
if (!closed.success) throw new Error(closed.error.message)
|
|
@@ -175,7 +175,7 @@ await scheduler.init({
|
|
|
175
175
|
|
|
176
176
|
可信服务端用 `execution.listLogFailures({ limit, offset })` 监控 pending(范围与 listUnknown 相同),用 `execution.repairLog(occurrenceId)` 从共享 journal 幂等补写;可跨节点、重启及并发调用。写入已提交但回执丢失时通过固定主键和完整内容核对去重。补写返回 `LOG_PERSIST_FAILED`(032)时继续保留 pending,并输出安全告警;全部数据库不可用且终态未提交时仍按 unknown 核对外部效果,不能假定存在可恢复结果。补写与日志保留均不调用业务 Hook。保留策略失败仅告警,不撤销已保存日志。`onTaskFinish` 是本地业务完成通知,其 unconfirmed 状态不是持久化确认;以 trigger 返回值及 execution.get 的 logPersistence 为准。
|
|
177
177
|
|
|
178
|
-
领取前竞争/跳过的 interrupted 仅为观测日志,没有可补写的终态;其写入未确认时返回 unconfirmed。JSON 大载荷保存完整结果和全部重试记录,避免 MySQL 的 255
|
|
178
|
+
领取前竞争/跳过的 interrupted 仅为观测日志,没有可补写的终态;其写入未确认时返回 unconfirmed。JSON 大载荷保存完整结果和全部重试记录,避免 MySQL 的 255 字符截断;初始化会自动执行版本化迁移,归档并复制旧日志;升级前先 dry-run、备份并停止旧写入,按模块 README 核对迁移影响。关闭会等待在途补写,先关闭 scheduler 再关闭数据库。
|
|
179
179
|
|
|
180
180
|
```ts
|
|
181
181
|
const result = await scheduler.trigger('cleanup', { idempotencyKey: 'cleanup-2026-10-23' })
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
## 运行环境
|
|
6
6
|
|
|
7
|
+
Local 的 key 路径拒绝文件符号链接和目录 junction,`.meta.json` / `.upload.tmp` 保留后缀不区分大小写。覆盖 put 清除旧元数据,copy 默认继承源元数据。浏览器根入口仅导出 Schema、类型和客户端辅助能力。
|
|
8
|
+
|
|
7
9
|
> ⚠️ **服务端模块**。`storage.file` / `storage.dir` / `storage.presign` 均在 Node.js 端使用。浏览器端通过预签名 URL 或 API 端点间接访问存储(见下方「浏览器端文件上传」)。
|
|
8
10
|
|
|
9
11
|
## 适用场景
|
|
@@ -53,9 +55,13 @@ MinIO、RustFS 使用同一个 S3 Provider:配置 `type: s3`、S3 API `endpoin
|
|
|
53
55
|
### 2. 初始化与关闭
|
|
54
56
|
|
|
55
57
|
```typescript
|
|
56
|
-
import {
|
|
58
|
+
import { core } from '@h-ai/core'
|
|
59
|
+
import { storage, StorageConfigSchema } from '@h-ai/storage'
|
|
57
60
|
|
|
58
|
-
|
|
61
|
+
const config = core.config.validate('storage', StorageConfigSchema)
|
|
62
|
+
if (!config.success) throw new Error(config.error.message)
|
|
63
|
+
const initialized = await storage.init(config.data)
|
|
64
|
+
if (!initialized.success) throw new Error(initialized.error.message)
|
|
59
65
|
// 使用后关闭
|
|
60
66
|
const closeResult = await storage.close()
|
|
61
67
|
if (!closeResult.success) {
|