@h-ai/cli 0.1.0-alpha.66 → 0.1.0-alpha.67
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 +66 -257
- package/README.ja.md +66 -257
- package/README.md +66 -257
- package/dist/{chunk-QYX6V7H2.js → chunk-7DSURTJ4.js} +22 -79
- package/dist/chunk-7DSURTJ4.js.map +1 -0
- package/dist/{chunk-FWN5B54L.js → chunk-AOQ4N657.js} +182 -10
- package/dist/chunk-AOQ4N657.js.map +1 -0
- package/dist/cli-config-templates-6AHNUGBN.js +5 -0
- package/dist/{cli-config-templates-3XSFIXBP.js.map → cli-config-templates-6AHNUGBN.js.map} +1 -1
- package/dist/{cli-skill-templates-TSYEQTCY.js → cli-skill-templates-FC3QRKDO.js} +3 -3
- package/dist/{cli-skill-templates-TSYEQTCY.js.map → cli-skill-templates-FC3QRKDO.js.map} +1 -1
- package/dist/index.js +435 -247
- package/dist/index.js.map +1 -1
- package/dist/{src-RDQ2EIBT.js → src-2V6VP4I5.js} +553 -119
- package/dist/src-2V6VP4I5.js.map +1 -0
- package/package.json +8 -8
- package/templates/README.md +48 -145
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +1 -1
- package/templates/apps/api/.dockerignore.hbs +14 -7
- package/templates/apps/api/.gitignore.hbs +7 -0
- package/templates/apps/api/apps/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/api/apps/{{projectName}}-service/README.md.hbs +28 -7
- package/templates/apps/api/apps/{{projectName}}-service/package.json.hbs +1 -2
- package/templates/apps/api/hai.build.json.hbs +1 -1
- package/templates/apps/api/images/app.Dockerfile.hbs +35 -0
- package/templates/apps/api/package.json.hbs +5 -0
- package/templates/apps/fullstack/.dockerignore.hbs +14 -9
- package/templates/apps/fullstack/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-app/index.html.hbs +1 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/public/runtime-config.js.hbs +4 -0
- package/templates/apps/fullstack/apps/{{projectName}}-app/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/apps/{{projectName}}-desktop/README.md.hbs +22 -4
- package/templates/apps/fullstack/apps/{{projectName}}-miniapp/README.md.hbs +16 -6
- package/templates/apps/fullstack/apps/{{projectName}}-web/README.md.hbs +23 -5
- package/templates/apps/fullstack/apps/{{projectName}}-web/vite.config.ts.hbs +1 -1
- package/templates/apps/fullstack/e2e/fullstack.spec.ts.hbs +11 -1
- package/templates/apps/fullstack/hai.build.json.hbs +2 -2
- package/templates/apps/fullstack/images/app.Dockerfile.hbs +36 -0
- package/templates/apps/fullstack/images/web.Dockerfile.hbs +34 -0
- package/templates/apps/fullstack/package.json.hbs +2 -0
- package/templates/apps/fullstack/packages/{{projectName}}-contract/README.md.hbs +23 -2
- package/templates/apps/fullstack/packages/{{projectName}}-serv/README.md.hbs +27 -6
- package/templates/apps/fullstack/packages/{{projectName}}-shared/.gitignore.hbs +7 -0
- package/templates/apps/fullstack/packages/{{projectName}}-shared/README.md.hbs +32 -11
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/components/AppShell.svelte.hbs +2 -2
- package/templates/apps/fullstack/packages/{{projectName}}-shared/src/lib/pages/settings.svelte.hbs +3 -3
- package/templates/apps/fullstack/packages/{{projectName}}-shared/tests/api-client.test.ts.hbs +2 -2
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +2 -2
- package/templates/apps/mobile-app/src/App.svelte.hbs +2 -2
- package/templates/apps/mobile-app/src/lib/pages/home.svelte.hbs +2 -2
- package/templates/apps/website/src/routes/+layout.svelte.hbs +3 -3
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +1 -1
- package/templates/base/.dockerignore.hbs +14 -8
- package/templates/base/.gitignore.hbs +7 -0
- package/templates/base/e2e/app.spec.ts.hbs +10 -0
- package/templates/base/hai.build.json.hbs +1 -1
- package/templates/base/{Dockerfile.hbs → images/app.Dockerfile.hbs} +19 -17
- package/templates/base/package.json.hbs +8 -3
- package/templates/base/vite.config.ts.hbs +3 -1
- package/templates/shared/.gitattributes +6 -0
- package/templates/skills/AGENTS.md +3 -3
- package/templates/skills/bridges/admin/AGENTS.md +1 -1
- package/templates/skills/bridges/api/AGENTS.md +1 -1
- package/templates/skills/bridges/fullstack/AGENTS.md.hbs +1 -1
- package/templates/skills/bridges/h5/AGENTS.md +1 -1
- package/templates/skills/bridges/mobile-app/AGENTS.md +1 -1
- package/templates/skills/bridges/website/AGENTS.md +1 -1
- package/templates/skills/hai-api-client/SKILL.md +14 -161
- package/templates/skills/hai-api-client/reference.md +166 -0
- package/templates/skills/hai-api-contract/SKILL.md +15 -273
- package/templates/skills/hai-api-contract/reference.md +278 -0
- package/templates/skills/hai-app-create/SKILL.md +2 -2
- package/templates/skills/hai-app-create/development.md +4 -4
- package/templates/skills/hai-app-create/reference.md +5 -4
- package/templates/skills/hai-app-review/SKILL.md +1 -1
- package/templates/skills/hai-app-tests/SKILL.md +4 -3
- package/templates/skills/hai-app-tests/reference.md +2 -2
- package/templates/skills/hai-app-tests/validation.md +31 -0
- package/templates/skills/hai-audit/SKILL.md +17 -142
- package/templates/skills/hai-audit/reference.md +145 -0
- package/templates/skills/hai-build/SKILL.md +14 -8
- package/templates/skills/hai-build/reference.md +10 -9
- package/templates/skills/hai-cache/SKILL.md +15 -182
- package/templates/skills/hai-cache/reference.md +187 -0
- package/templates/skills/hai-capacitor/SKILL.md +4 -3
- package/templates/skills/hai-capacitor/reference.md +6 -4
- package/templates/skills/hai-ci/SKILL.md +2 -0
- package/templates/skills/hai-core/SKILL.md +3 -3
- package/templates/skills/hai-crypto/SKILL.md +16 -252
- package/templates/skills/hai-crypto/reference.md +256 -0
- package/templates/skills/hai-datapipe/SKILL.md +15 -196
- package/templates/skills/hai-datapipe/reference.md +202 -0
- package/templates/skills/hai-deploy/SKILL.md +20 -55
- package/templates/skills/hai-deploy/middleware.md +5 -11
- package/templates/skills/hai-framework-sync/SKILL.md +2 -2
- package/templates/skills/hai-framework-sync/sync.md +1 -1
- package/templates/skills/hai-iam/SKILL.md +1 -3
- package/templates/skills/hai-kit/SKILL.md +12 -167
- package/templates/skills/hai-kit/reference.md +172 -0
- package/templates/skills/hai-payment/SKILL.md +19 -251
- package/templates/skills/hai-payment/reference.md +256 -0
- package/templates/skills/hai-pr-review/SKILL.md +4 -4
- package/templates/skills/hai-reach/SKILL.md +2 -2
- package/templates/skills/hai-reldb/SKILL.md +1 -1
- package/templates/skills/hai-scheduler/SKILL.md +15 -195
- package/templates/skills/hai-scheduler/reference.md +202 -0
- package/templates/skills/hai-serv/SKILL.md +2 -2
- package/templates/skills/hai-spec-implement/SKILL.md +1 -1
- package/templates/skills/hai-spec-implement/workflow.md +3 -3
- package/templates/skills/hai-storage/SKILL.md +17 -238
- package/templates/skills/hai-storage/reference.md +243 -0
- package/templates/skills/hai-ui/SKILL.md +1 -1
- package/templates/skills/hai-ui/reference.md +9 -5
- package/templates/skills/hai-vecdb/SKILL.md +2 -2
- package/dist/chunk-FWN5B54L.js.map +0 -1
- package/dist/chunk-QYX6V7H2.js.map +0 -1
- package/dist/cli-config-templates-3XSFIXBP.js +0 -5
- package/dist/src-RDQ2EIBT.js.map +0 -1
- package/templates/apps/api/Dockerfile.hbs +0 -35
- package/templates/apps/fullstack/Dockerfile.hbs +0 -57
- /package/templates/apps/fullstack/{docker → images/web}/19-runtime-config.envsh.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/nginx.conf.hbs +0 -0
- /package/templates/apps/fullstack/{docker → images/web}/runtime-config.js.template.hbs +0 -0
- /package/templates/base/{docker → images/web}/nginx.conf.hbs +0 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# hai-api-client 参考
|
|
2
|
+
|
|
3
|
+
> `@h-ai/api-client` 是跨端 typed API client。公共 HTTP API 由 `@h-ai/api-contract` 定义,服务端由 `@h-ai/serv` 挂载,客户端直接调用 `apiClient.<domain>.<group>.<operation>()`。
|
|
4
|
+
|
|
5
|
+
## 使用步骤
|
|
6
|
+
|
|
7
|
+
### 1. 初始化
|
|
8
|
+
|
|
9
|
+
浏览器默认使用 httpOnly cookie 存储(推荐):
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { apiClient } from '@h-ai/api-client'
|
|
13
|
+
|
|
14
|
+
await apiClient.init({
|
|
15
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
16
|
+
auth: {},
|
|
17
|
+
})
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
启用传输加密时,只传顶层 `crypto` 句柄;api-client 内部自动调用 `crypto.transport.createClient()`:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { apiClient } from '@h-ai/api-client'
|
|
24
|
+
import { crypto } from '@h-ai/crypto'
|
|
25
|
+
|
|
26
|
+
await crypto.init()
|
|
27
|
+
|
|
28
|
+
await apiClient.init({
|
|
29
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
30
|
+
auth: {},
|
|
31
|
+
transport: { crypto }, // 默认协商路径:/api/v1/_hai/key-exchange
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
App/Capacitor 场景传入自定义 TokenStorage:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { createCapacitorTokenStorage } from '@h-ai/capacitor'
|
|
39
|
+
|
|
40
|
+
await apiClient.init({
|
|
41
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
42
|
+
auth: { storage: createCapacitorTokenStorage() },
|
|
43
|
+
})
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
> `createCapacitorTokenStorage()` 仅在 Android/iOS 原生环境 + `@aparajita/capacitor-secure-storage` 已安装时持久化 token;纯 Web 不会回退到 `localStorage`。
|
|
47
|
+
|
|
48
|
+
SSR / Node.js 测试场景请显式传入内存存储:
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { apiClient } from '@h-ai/api-client'
|
|
52
|
+
|
|
53
|
+
await apiClient.init({
|
|
54
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
55
|
+
auth: { storage: apiClient.tokenStorage.memory() },
|
|
56
|
+
})
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### 2. 调用 typed API
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const login = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
63
|
+
if (login.success) {
|
|
64
|
+
// 默认 httpOnly cookie 模式下,refresh token 由服务端 Set-Cookie 管理。
|
|
65
|
+
await apiClient.auth.setTokens(login.data.tokens)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const me = await apiClient.iam.auth.currentUser()
|
|
69
|
+
if (me.success) {
|
|
70
|
+
// currentUser 返回服务端最新角色/权限;不要把权限快照持久化到 localStorage。
|
|
71
|
+
const { roles, permissions, ...user } = me.data
|
|
72
|
+
}
|
|
73
|
+
const upload = await apiClient.storage.presignedUrls.createUpload({ key: 'avatar.png' })
|
|
74
|
+
const answer = await apiClient.ai.chats.sendMessage({ message: 'hello' })
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### 3. 自定义 contract
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import { apiClient } from '@h-ai/api-client'
|
|
81
|
+
import { apiContract } from '@h-ai/api-contract'
|
|
82
|
+
|
|
83
|
+
const contract = apiContract.create({ iam: apiContract.iam })
|
|
84
|
+
const client = apiClient.create(contract)
|
|
85
|
+
await client.init({ baseUrl: 'https://api.example.com/api/v1' })
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`client.config` / `apiClient.config` 返回的是脱敏后的运行配置快照;`baseUrl` 的内嵌凭证与 `headers.authorization` 等敏感值会自动隐藏。
|
|
89
|
+
|
|
90
|
+
### 4. 关闭
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
await apiClient.close()
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
> 已初始化后再次 `init()` 会返回 `CONFIG_ERROR`,不会覆盖现有 client;切换 `baseUrl` / `fetch` / `transport` 前必须先 `close()`,避免旧 transport 会话泄漏。
|
|
97
|
+
|
|
98
|
+
## Token Storage 适配器
|
|
99
|
+
|
|
100
|
+
| 工厂调用 | 存储位置 | 适用场景 |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| `apiClient.tokenStorage.httpOnlyCookie()` | httpOnly cookie(服务端管理) | **默认**;浏览器端推荐,refresh token 不暴露给 JS |
|
|
103
|
+
| `apiClient.tokenStorage.memory()` | 内存 | SSR / Node.js 单元测试(需显式传入) |
|
|
104
|
+
| `apiClient.tokenStorage.localStorage()` | localStorage | 非敏感场景(有 XSS 风险,生产不推荐) |
|
|
105
|
+
|
|
106
|
+
### httpOnly Cookie 模式
|
|
107
|
+
|
|
108
|
+
Access token 存在内存中,refresh token 由服务端通过 `HttpOnly` cookie 管理,浏览器 JS 无法读取,可有效防止 XSS 窃取 token。
|
|
109
|
+
|
|
110
|
+
**前提**:服务端需配置 `serv.createApp({ iam, refreshCookie: {} })`,参见 `hai-serv` skill。
|
|
111
|
+
|
|
112
|
+
启用 `transport` 时,登录、业务请求以及 401 后的自动刷新都会复用同一个加密 fetch 链路;不要在业务代码中手写明文 `/auth/refresh` 请求。
|
|
113
|
+
|
|
114
|
+
自动刷新识别真实 HTTP 401(`Response.status`),以及 HTTP 200 的有效失败 HaiResult(`success=false`、字符串 `error.code/message`、`error.httpStatus=401`),成功后只重试一次原请求。内置登录、注册与配置的 `auth.refreshPath` 排除自动刷新,403/500 等其它失败也不刷新。serv 普通 procedure 的 HTTP 状态仍为 200;调用方须检查最终 `success/error.code` 并处理刷新失败后的登录失效。
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { apiClient } from '@h-ai/api-client'
|
|
118
|
+
|
|
119
|
+
// apiClient.tokenStorage.httpOnlyCookie() 是默认存储,无需显式指定
|
|
120
|
+
await apiClient.init({
|
|
121
|
+
baseUrl: 'https://api.example.com/api/v1',
|
|
122
|
+
auth: {},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// 登录后设置 access token(refresh token 由服务端写入 cookie,前端无需操作)
|
|
126
|
+
const login = await apiClient.iam.auth.login({ identifier: 'alice', password: 'secret' })
|
|
127
|
+
if (login.success) {
|
|
128
|
+
await apiClient.auth.setTokens(login.data.tokens)
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## 核心 API
|
|
133
|
+
|
|
134
|
+
| API | 用途 |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `apiClient.init(config)` | 初始化默认 API Service client;重复初始化返回 `CONFIG_ERROR` |
|
|
137
|
+
| `apiClient.close()` | 清理 client 状态 |
|
|
138
|
+
| `apiClient.auth.setTokens(tokens)` | 写入 access token;非 httpOnly 存储会同时写入 refresh token |
|
|
139
|
+
| `apiClient.auth.clear()` | 清理 Token |
|
|
140
|
+
| `apiClient.auth.onTokenRefreshed(cb)` | 监听刷新结果 |
|
|
141
|
+
| `apiClient.create(contract)` | 创建自定义 typed client |
|
|
142
|
+
| `apiClient.tokenStorage.{memory,localStorage,httpOnlyCookie}()` | TokenStorage 工厂 |
|
|
143
|
+
|
|
144
|
+
### Transport 配置
|
|
145
|
+
|
|
146
|
+
| 字段 | 说明 |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `transport.crypto` | 已初始化的 `@h-ai/crypto` 实例 |
|
|
149
|
+
| `transport.keyExchangePath` | 相对于 `baseUrl` 的协商子路径,默认 `/_hai/key-exchange` |
|
|
150
|
+
|
|
151
|
+
服务端必须对应启用 `serv.createApp({ transport: { crypto } })`。
|
|
152
|
+
自动刷新会沿用 `transport.encryptedFetch`,并在 httpOnly cookie 模式下发送空请求体 + `credentials: 'include'`,由服务端从 cookie 读取 refresh token。
|
|
153
|
+
|
|
154
|
+
## 常见模式
|
|
155
|
+
|
|
156
|
+
- `baseUrl` 通常包含服务端 `apiPrefix`,例如 `/api/v1`。
|
|
157
|
+
- 业务错误不 throw,统一判断 `HaiResult.success`。
|
|
158
|
+
- 客户端不导入服务端 `procedures` 或 `@h-ai/serv`。
|
|
159
|
+
- 客户端不导入 `@h-ai/crypto` 的内部 transport 工厂,只通过 `transport: { crypto }` 装配。
|
|
160
|
+
- 前端只依赖 `@h-ai/api-client` 和 contract 类型,不依赖业务模块实现。
|
|
161
|
+
- httpOnly cookie 模式需服务端与客户端同步配置,跨 Origin 请求需正确设置 CORS 与 credentials;`SameSite=Strict` 限制跨站 Cookie,跨域不等于跨站。
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
## 配置驱动认证页面
|
|
165
|
+
|
|
166
|
+
`apiClient.createAuthActions(client)` 为含标准 iam contract 的客户端提供 publicConfig、login、register、sendOtp、requestPasswordReset、confirmPasswordReset。login 输入使用 identifier/password/method/code,method 为 password、ldap、email、sms。成功认证自动调用既有安全 TokenStorage;应用只需把动作传给 AuthPanel,并在 onauthenticated 中刷新自己的用户状态或导航。
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: hai-api-contract
|
|
3
|
-
description: "
|
|
3
|
+
description: "新增、组合或修改共享 HTTP 接口的路径与输入输出 Schema,供 serv 与 typed client 共用。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# hai-api-contract
|
|
@@ -15,279 +15,21 @@ description: "使用 @h-ai/api-contract 定义、组合和扩展共享 HTTP cont
|
|
|
15
15
|
| 输出 | 供 serv 实现和 api-client 消费的 contract |
|
|
16
16
|
| 限制 | 纯定义,无 init/close;不包含服务端实现或密钥。业务路径属于 contract,公共传输配置属于 transport。 |
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## 使用路径
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
1. 查当前公开 exports 与已有领域 contract,确认业务路径唯一来源。
|
|
21
|
+
2. 在 contract 中声明 method/path 与输入输出 Schema;只导出真正跨接口或跨层复用的 Schema。
|
|
22
|
+
3. 用 `apiContract.create` 组合领域;不把服务实现、私有配置或传输前缀写进 contract。
|
|
23
|
+
4. 同步 serv procedures、typed client、调用方与失败/校验测试;普通业务输出按当前 HaiResult 契约。
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
## 按需参考
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
按本次操作读取对应小节;以已安装版本的公开类型核验签名,示例中的业务变量由应用提供。
|
|
25
28
|
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
## 使用步骤
|
|
34
|
-
|
|
35
|
-
### 1. 组合应用级 contract
|
|
36
|
-
|
|
37
|
-
```typescript
|
|
38
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
39
|
-
|
|
40
|
-
// 按需组合,传 false/undefined 的领域不会出现在 contract 或 OpenAPI spec 中
|
|
41
|
-
export const myContract = apiContract.create({
|
|
42
|
-
iam: apiContract.iam,
|
|
43
|
-
storage: apiContract.storage,
|
|
44
|
-
ai: apiContract.ai,
|
|
45
|
-
payment: false, // 禁用领域
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
// myContract.iam.auth.login
|
|
49
|
-
// myContract.storage.presignedUrls.createUpload
|
|
50
|
-
// myContract.ai.chats.createCompletion
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### 2. 组合自定义 contract
|
|
54
|
-
|
|
55
|
-
```typescript
|
|
56
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
57
|
-
|
|
58
|
-
export const myContract = apiContract.create({
|
|
59
|
-
iam: apiContract.iam,
|
|
60
|
-
storage: apiContract.storage,
|
|
61
|
-
payment: false, // 禁用领域(不会出现在 contract 或 OpenAPI spec 中)
|
|
62
|
-
})
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
### 3. 定义新领域 contract
|
|
66
|
-
|
|
67
|
-
```typescript
|
|
68
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
69
|
-
import { z } from 'zod'
|
|
70
|
-
|
|
71
|
-
// 输入 Schema
|
|
72
|
-
const WidgetCreateInputSchema = z.object({
|
|
73
|
-
name: z.string().min(1),
|
|
74
|
-
description: z.string().optional(),
|
|
75
|
-
})
|
|
76
|
-
|
|
77
|
-
// 同一 contract 内两个接口复用,因此保留为文件私有常量,不从 schemas 导出。
|
|
78
|
-
const WidgetOutputSchema = apiContract.haiResultSchema(z.object({
|
|
79
|
-
id: z.string(),
|
|
80
|
-
name: z.string(),
|
|
81
|
-
createdAt: z.string(),
|
|
82
|
-
}))
|
|
83
|
-
|
|
84
|
-
// 领域 contract
|
|
85
|
-
export const widgetContract = {
|
|
86
|
-
widget: {
|
|
87
|
-
create: apiContract
|
|
88
|
-
.route({
|
|
89
|
-
method: 'POST',
|
|
90
|
-
path: '/widgets',
|
|
91
|
-
operationId: 'widget.create',
|
|
92
|
-
summary: 'Create widget',
|
|
93
|
-
tags: ['widget'],
|
|
94
|
-
})
|
|
95
|
-
.input(WidgetCreateInputSchema)
|
|
96
|
-
.output(WidgetOutputSchema),
|
|
97
|
-
|
|
98
|
-
getById: apiContract
|
|
99
|
-
.route({
|
|
100
|
-
method: 'GET',
|
|
101
|
-
path: '/widgets/{id}',
|
|
102
|
-
operationId: 'widget.getById',
|
|
103
|
-
summary: 'Get widget by id',
|
|
104
|
-
tags: ['widget'],
|
|
105
|
-
})
|
|
106
|
-
.input(z.object({ id: z.string() }))
|
|
107
|
-
.output(WidgetOutputSchema),
|
|
108
|
-
},
|
|
109
|
-
}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### 4. 将新 contract 挂入应用级 contract
|
|
113
|
-
|
|
114
|
-
```typescript
|
|
115
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
116
|
-
import { widgetContract } from './widget-contract.js'
|
|
117
|
-
|
|
118
|
-
export const appContract = apiContract.create({
|
|
119
|
-
iam: apiContract.iam,
|
|
120
|
-
widget: widgetContract,
|
|
121
|
-
})
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
### 5. 传给 `@h-ai/serv`(服务端)
|
|
125
|
-
|
|
126
|
-
```typescript
|
|
127
|
-
import { serv } from '@h-ai/serv'
|
|
128
|
-
import { appContract } from './app-contract.js'
|
|
129
|
-
import { createMyProcedures } from './procedures/widget.js'
|
|
130
|
-
|
|
131
|
-
const app = serv.createApp({
|
|
132
|
-
contract: appContract,
|
|
133
|
-
procedures: {
|
|
134
|
-
iam: createIamProcedures({ iam }),
|
|
135
|
-
widget: createMyProcedures(),
|
|
136
|
-
},
|
|
137
|
-
http: { apiPrefix: '/api/v1' },
|
|
138
|
-
})
|
|
139
|
-
```
|
|
140
|
-
|
|
141
|
-
## 核心 API
|
|
142
|
-
|
|
143
|
-
### `apiContract.create(options)` — 组合领域 contract
|
|
144
|
-
|
|
145
|
-
```typescript
|
|
146
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
147
|
-
|
|
148
|
-
const contract = apiContract.create({
|
|
149
|
-
iam: apiContract.iam, // 启用:纳入 contract
|
|
150
|
-
storage: false, // 禁用:不出现在 OpenAPI spec 中
|
|
151
|
-
custom: myContract, // 自定义领域
|
|
152
|
-
})
|
|
153
|
-
// 类型自动收窄,被禁用的 key 不出现在 contract 类型中
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
### 内置领域 contract
|
|
157
|
-
|
|
158
|
-
| 访问路径 | 领域 | 功能概述 |
|
|
159
|
-
| --- | --- | --- |
|
|
160
|
-
| `apiContract.iam` | IAM | auth(登录/登出/OTP/刷新/注册/改密码)、users、roles、permissions Admin CRUD |
|
|
161
|
-
| `apiContract.storage` | Storage | presignedUrls(上传/下载/批量)、files(元数据查询/删除) |
|
|
162
|
-
| `apiContract.ai` | AI | chats(completion/流式)、knowledge(上传/查询) |
|
|
163
|
-
| `apiContract.payment` | Payment | orders、subscriptions |
|
|
164
|
-
|
|
165
|
-
### 内置 contract 路径示例
|
|
166
|
-
|
|
167
|
-
```typescript
|
|
168
|
-
const myContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })
|
|
169
|
-
|
|
170
|
-
// IAM
|
|
171
|
-
myContract.iam.auth.login // POST /auth/login
|
|
172
|
-
myContract.iam.auth.logout // POST /auth/logout
|
|
173
|
-
myContract.iam.users.list // GET /iam/users
|
|
174
|
-
myContract.iam.roles.create // POST /iam/roles
|
|
175
|
-
|
|
176
|
-
// Storage
|
|
177
|
-
myContract.storage.presignedUrls.createUpload // POST /storage/presigned-urls/upload
|
|
178
|
-
|
|
179
|
-
// AI
|
|
180
|
-
myContract.ai.chats.createCompletion // POST /ai/chats/completion
|
|
181
|
-
```
|
|
182
|
-
|
|
183
|
-
### `apiContract.haiResultSchema(dataSchema)` — 标准输出包装
|
|
184
|
-
|
|
185
|
-
所有 HTTP 输出必须通过 `apiContract.haiResultSchema` 包装为 `HaiResult<T>`,与 `@h-ai/core` 的 `ok()/err()` 返回结构一致:
|
|
186
|
-
|
|
187
|
-
```typescript
|
|
188
|
-
import { apiContract } from '@h-ai/api-contract'
|
|
189
|
-
import { z } from 'zod'
|
|
190
|
-
|
|
191
|
-
const getWidget = apiContract
|
|
192
|
-
.route({ method: 'GET', path: '/widgets/{id}' })
|
|
193
|
-
.output(apiContract.haiResultSchema(z.object({
|
|
194
|
-
id: z.string(),
|
|
195
|
-
name: z.string(),
|
|
196
|
-
})))
|
|
197
|
-
|
|
198
|
-
// 等价于:
|
|
199
|
-
// { success: true, data: { id: string, name: string } }
|
|
200
|
-
// | { success: false, error: HaiError }
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
### `apiContract.pathOf(procedure)` — 读取业务路径
|
|
204
|
-
|
|
205
|
-
```typescript
|
|
206
|
-
const refreshPath = apiContract.pathOf(apiContract.iam.auth.refresh)
|
|
207
|
-
// '/auth/refresh'
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
middleware、client 与 E2E 需要业务路径时从 contract 元数据读取,不维护第二份 route 常量。服务端挂载时再与 `_serv.yml` 的 `http.apiPrefix` 拼接。
|
|
211
|
-
|
|
212
|
-
### 公共 Schema 工具
|
|
213
|
-
|
|
214
|
-
| 访问路径 | 说明 |
|
|
215
|
-
| --- | --- |
|
|
216
|
-
| `apiContract.haiResultSchema(dataSchema)` | `HaiResult<T>` 输出包装 |
|
|
217
|
-
| `apiContract.voidResultSchema` | `HaiResult<void>` 空结果包装 |
|
|
218
|
-
| `apiContract.paginatedSchema(itemSchema)` | 分页列表输出 Schema |
|
|
219
|
-
| `HaiErrorSchema` | HaiError 公共字段 Schema |
|
|
220
|
-
| `PaginationQuerySchema` | 分页参数 Schema(可选 page/pageSize,pageSize 上限 100;默认值由服务端处理) |
|
|
221
|
-
|
|
222
|
-
## Contract 定义规范
|
|
223
|
-
|
|
224
|
-
### 必须遵循
|
|
225
|
-
|
|
226
|
-
- **输出必须包装为 `apiContract.haiResultSchema`** — 客户端依赖 `success` 判断成功/失败
|
|
227
|
-
- **业务 path 直接写在对应 `*-contract.ts`** — 禁止新增 `*-routes.ts` 或业务路径常量
|
|
228
|
-
- **一次性 `*OutputSchema` 直接内联** — 不在 schemas 文件声明或导出
|
|
229
|
-
- **同一 contract 内重复使用的输出可设私有常量** — 不得为复用扩大公共导出
|
|
230
|
-
- **公共 HTTP 传输配置以配置文件为准** — API 前缀、健康检查、OpenAPI/docs、密钥协商路径与跨端 Header 放在 service 的 `_serv.yml`,不放 contract 包
|
|
231
|
-
- **schemas 只保留真实复用结构** — 至少被多个接口或 contract/procedure 等不同层共同消费
|
|
232
|
-
- **`operationId` 全局唯一** — 格式为 `domain.resource.action`,如 `iam.auth.login`
|
|
233
|
-
- **`tags` 至少包含领域名** — 供 OpenAPI 文档分组
|
|
234
|
-
- **输入 Schema 用 Zod v4** — 与 `@orpc/zod/zod4` 的 `ZodToJsonSchemaConverter` 配套
|
|
235
|
-
- **Contract 不含业务逻辑** — 只有 Schema + 路由元数据,没有 `async function`
|
|
236
|
-
|
|
237
|
-
### 禁止
|
|
238
|
-
|
|
239
|
-
- ❌ Contract 文件中 `import` 任何业务模块(`@h-ai/iam`、`@h-ai/storage` 等)
|
|
240
|
-
- ❌ 在 contract 中使用 `z.any()` 或 `z.unknown()` 作为输出类型
|
|
241
|
-
- ❌ 重复定义已存在的领域 contract
|
|
242
|
-
- ❌ 导出只被单个 contract 接口使用的 `*OutputSchema`
|
|
243
|
-
- ❌ 用独立 `*-routes.ts` 维护业务接口路径
|
|
244
|
-
|
|
245
|
-
### 文件职责
|
|
246
|
-
|
|
247
|
-
| 文件 | 只负责 |
|
|
248
|
-
| --- | --- |
|
|
249
|
-
| `*-contract.ts` | 业务接口 path、method、输入与输出边界 |
|
|
250
|
-
| `*-schemas.ts` | 跨接口或跨层复用的输入、实体、数据结构 |
|
|
251
|
-
| service `config/_serv.yml` | API 前缀、基础设施端点、跨端 Header 与加密 transport 配置 |
|
|
252
|
-
|
|
253
|
-
## 常见模式
|
|
254
|
-
|
|
255
|
-
### 带分页的列表接口
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
import { apiContract, PaginationQuerySchema } from '@h-ai/api-contract'
|
|
259
|
-
import { z } from 'zod'
|
|
260
|
-
|
|
261
|
-
const WidgetSchema = z.object({ id: z.string(), name: z.string() })
|
|
262
|
-
|
|
263
|
-
const listWidgets = apiContract
|
|
264
|
-
.route({ method: 'GET', path: '/widgets', operationId: 'widget.list', tags: ['widget'] })
|
|
265
|
-
.input(PaginationQuerySchema.extend({ keyword: z.string().optional() }))
|
|
266
|
-
.output(apiContract.haiResultSchema(apiContract.paginatedSchema(WidgetSchema)))
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
### 路径参数
|
|
270
|
-
|
|
271
|
-
```typescript
|
|
272
|
-
// 路径中的 {id} 对应 input 中的 id 字段
|
|
273
|
-
const getWidget = apiContract
|
|
274
|
-
.route({ method: 'GET', path: '/widgets/{id}', operationId: 'widget.getById', tags: ['widget'] })
|
|
275
|
-
.input(z.object({ id: z.string() }))
|
|
276
|
-
.output(apiContract.haiResultSchema(WidgetSchema))
|
|
277
|
-
```
|
|
278
|
-
|
|
279
|
-
### 无输入的端点
|
|
280
|
-
|
|
281
|
-
```typescript
|
|
282
|
-
const versionContract = apiContract
|
|
283
|
-
.route({ method: 'GET', path: '/system/version', operationId: 'system.version', tags: ['system'] })
|
|
284
|
-
.output(apiContract.haiResultSchema(z.object({ version: z.string() })))
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
## 测试
|
|
288
|
-
|
|
289
|
-
```bash
|
|
290
|
-
pnpm --filter @h-ai/api-contract test
|
|
291
|
-
pnpm --filter @h-ai/api-contract typecheck
|
|
292
|
-
pnpm --filter @h-ai/api-contract lint
|
|
293
|
-
```
|
|
29
|
+
- [运行环境](reference.md#运行环境)
|
|
30
|
+
- [适用场景](reference.md#适用场景)
|
|
31
|
+
- [使用步骤](reference.md#使用步骤)
|
|
32
|
+
- [核心 API](reference.md#核心-api)
|
|
33
|
+
- [Contract 定义规范](reference.md#contract-定义规范)
|
|
34
|
+
- [常见模式](reference.md#常见模式)
|
|
35
|
+
- [测试](reference.md#测试)
|