@h-ai/cli 0.1.0-alpha.12 → 0.1.0-alpha.15

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.
@@ -0,0 +1,272 @@
1
+ ---
2
+ name: hai-api-contract
3
+ description: 使用 @h-ai/api-contract 定义或扩展 oRPC HTTP API 契约;当需求涉及定义新领域 contract、组合应用级 contract、使用内置 IAM/Storage/AI/Payment 契约、编写自定义 procedure 输入输出 Schema 或为 @h-ai/api-client/@h-ai/serv 提供接口真相源时使用。
4
+ ---
5
+
6
+ # hai-api-contract
7
+
8
+ > `@h-ai/api-contract` 是 hai-framework 的公共 HTTP API 契约包,使用 oRPC contract + Zod v4 作为唯一接口真相源。本包只描述接口边界,不包含任何 procedure 实现。
9
+
10
+ ---
11
+
12
+ ## 运行环境
13
+
14
+ > ✅ **纯定义包,无运行时依赖。** 可在客户端、服务端、测试环境直接引用。
15
+
16
+ ---
17
+
18
+ ## 适用场景
19
+
20
+ - 定义新领域的 oRPC contract(输入/输出 Schema + HTTP 路由元数据)
21
+ - 组合多个领域 contract 为应用级 contract
22
+ - 向 `@h-ai/serv` 提供 contract(装配为 HTTP API)
23
+ - 向 `@h-ai/api-client` 提供 contract(生成类型安全客户端)
24
+ - 复用内置领域 contract(iam/storage/ai/payment)
25
+ - 复用公共 Schema 工厂(`apiContract.haiResultSchema`、分页 Schema)
26
+
27
+ ---
28
+
29
+ ## 使用步骤
30
+
31
+ ### 1. 组合应用级 contract
32
+
33
+ ```typescript
34
+ import { apiContract } from '@h-ai/api-contract'
35
+
36
+ // 按需组合,传 false/undefined 的领域不会出现在 contract 或 OpenAPI spec 中
37
+ export const myContract = apiContract.create({
38
+ iam: apiContract.iam,
39
+ storage: apiContract.storage,
40
+ ai: apiContract.ai,
41
+ payment: false, // 禁用领域
42
+ })
43
+
44
+ // myContract.iam.auth.login
45
+ // myContract.storage.presignedUrls.createUpload
46
+ // myContract.ai.chats.createCompletion
47
+ ```
48
+
49
+ ### 2. 组合自定义 contract
50
+
51
+ ```typescript
52
+ import { apiContract } from '@h-ai/api-contract'
53
+
54
+ export const myContract = apiContract.create({
55
+ iam: apiContract.iam,
56
+ storage: apiContract.storage,
57
+ payment: false, // 禁用领域(不会出现在 contract 或 OpenAPI spec 中)
58
+ })
59
+ ```
60
+
61
+ ### 3. 定义新领域 contract
62
+
63
+ ```typescript
64
+ import { apiContract } from '@h-ai/api-contract'
65
+ import { z } from 'zod'
66
+
67
+ // 输入 Schema
68
+ const WidgetCreateInputSchema = z.object({
69
+ name: z.string().min(1),
70
+ description: z.string().optional(),
71
+ })
72
+
73
+ // 输出 Schema(统一包装为 HaiResult<T>)
74
+ const WidgetOutputSchema = apiContract.haiResultSchema(z.object({
75
+ id: z.string(),
76
+ name: z.string(),
77
+ createdAt: z.string(),
78
+ }))
79
+
80
+ // 领域 contract
81
+ export const widgetContract = {
82
+ widget: {
83
+ create: apiContract
84
+ .route({
85
+ method: 'POST',
86
+ path: '/widgets',
87
+ operationId: 'widget.create',
88
+ summary: 'Create widget',
89
+ tags: ['widget'],
90
+ })
91
+ .input(WidgetCreateInputSchema)
92
+ .output(WidgetOutputSchema),
93
+
94
+ getById: apiContract
95
+ .route({
96
+ method: 'GET',
97
+ path: '/widgets/{id}',
98
+ operationId: 'widget.getById',
99
+ summary: 'Get widget by id',
100
+ tags: ['widget'],
101
+ })
102
+ .input(z.object({ id: z.string() }))
103
+ .output(WidgetOutputSchema),
104
+ },
105
+ }
106
+ ```
107
+
108
+ ### 4. 将新 contract 挂入应用级 contract
109
+
110
+ ```typescript
111
+ import { apiContract } from '@h-ai/api-contract'
112
+ import { widgetContract } from './widget-contract.js'
113
+
114
+ export const appContract = apiContract.create({
115
+ iam: apiContract.iam,
116
+ widget: widgetContract,
117
+ })
118
+ ```
119
+
120
+ ### 5. 传给 `@h-ai/serv`(服务端)
121
+
122
+ ```typescript
123
+ import { serv } from '@h-ai/serv'
124
+ import { appContract } from './app-contract.js'
125
+ import { createMyProcedures } from './procedures/widget.js'
126
+
127
+ const app = serv.createApp({
128
+ contract: appContract,
129
+ procedures: {
130
+ iam: createIamProcedures({ iam }),
131
+ widget: createMyProcedures(),
132
+ },
133
+ http: { apiPrefix: '/api/v1' },
134
+ })
135
+ ```
136
+
137
+ ---
138
+
139
+ ## 核心 API
140
+
141
+ ### `apiContract.create(options)` — 组合领域 contract
142
+
143
+ ```typescript
144
+ import { apiContract } from '@h-ai/api-contract'
145
+
146
+ const contract = apiContract.create({
147
+ iam: apiContract.iam, // 启用:纳入 contract
148
+ storage: false, // 禁用:不出现在 OpenAPI spec 中
149
+ custom: myContract, // 自定义领域
150
+ })
151
+ // 类型自动收窄,被禁用的 key 不出现在 contract 类型中
152
+ ```
153
+
154
+ ### 内置领域 contract
155
+
156
+ | 访问路径 | 领域 | 功能概述 |
157
+ | --- | --- | --- |
158
+ | `apiContract.iam` | IAM | auth(登录/登出/OTP/刷新/注册/改密码)、users、roles、permissions Admin CRUD |
159
+ | `apiContract.storage` | Storage | presignedUrls(上传/下载/批量)、files(元数据查询/删除) |
160
+ | `apiContract.ai` | AI | chats(completion/流式)、knowledge(上传/查询) |
161
+ | `apiContract.payment` | Payment | orders、subscriptions |
162
+
163
+ ### 内置 contract 路径示例
164
+
165
+ ```typescript
166
+ const myContract = apiContract.create({ iam: apiContract.iam, storage: apiContract.storage, ai: apiContract.ai })
167
+
168
+ // IAM
169
+ myContract.iam.auth.login // POST /auth/login
170
+ myContract.iam.auth.logout // POST /auth/logout
171
+ myContract.iam.users.list // GET /iam/users
172
+ myContract.iam.roles.create // POST /iam/roles
173
+
174
+ // Storage
175
+ myContract.storage.presignedUrls.createUpload // POST /storage/presigned-urls/upload
176
+
177
+ // AI
178
+ myContract.ai.chats.createCompletion // POST /ai/chats/completion
179
+ ```
180
+
181
+ ### `apiContract.haiResultSchema(dataSchema)` — 标准输出包装
182
+
183
+ 所有 HTTP 输出必须通过 `apiContract.haiResultSchema` 包装为 `HaiResult<T>`,与 `@h-ai/core` 的 `ok()/err()` 返回结构一致:
184
+
185
+ ```typescript
186
+ import { apiContract } from '@h-ai/api-contract'
187
+ import { z } from 'zod'
188
+
189
+ const OutputSchema = apiContract.haiResultSchema(z.object({
190
+ id: z.string(),
191
+ name: z.string(),
192
+ }))
193
+
194
+ // 等价于:
195
+ // { success: true, data: { id: string, name: string } }
196
+ // | { success: false, error: HaiError }
197
+ ```
198
+
199
+ ### 公共 Schema 工具
200
+
201
+ | 访问路径 | 说明 |
202
+ | --- | --- |
203
+ | `apiContract.haiResultSchema(dataSchema)` | `HaiResult<T>` 输出包装 |
204
+ | `apiContract.voidResultSchema` | `HaiResult<void>` 空结果包装 |
205
+ | `apiContract.paginatedSchema(itemSchema)` | 分页列表输出 Schema |
206
+ | `HaiErrorSchema` | HaiError 公共字段 Schema |
207
+ | `PaginationInputSchema` | 分页参数 Schema(page/limit) |
208
+ | `PaginationOutputSchema` | 分页元数据 Schema(total/page/limit) |
209
+
210
+ ---
211
+
212
+ ## Contract 定义规范
213
+
214
+ ### 必须遵循
215
+
216
+ - **输出必须包装为 `apiContract.haiResultSchema`** — 客户端依赖 `success` 判断成功/失败
217
+ - **`operationId` 全局唯一** — 格式为 `domain.resource.action`,如 `iam.auth.login`
218
+ - **`tags` 至少包含领域名** — 供 OpenAPI 文档分组
219
+ - **输入 Schema 用 Zod v4** — 与 `@orpc/zod/zod4` 的 `ZodToJsonSchemaConverter` 配套
220
+ - **Contract 不含业务逻辑** — 只有 Schema + 路由元数据,没有 `async function`
221
+
222
+ ### 禁止
223
+
224
+ - ❌ Contract 文件中 `import` 任何业务模块(`@h-ai/iam`、`@h-ai/storage` 等)
225
+ - ❌ 在 contract 中使用 `z.any()` 或 `z.unknown()` 作为输出类型
226
+ - ❌ 重复定义已存在的领域 contract
227
+
228
+ ---
229
+
230
+ ## 常见模式
231
+
232
+ ### 带分页的列表接口
233
+
234
+ ```typescript
235
+ import { apiContract, PaginationInputSchema } from '@h-ai/api-contract'
236
+ import { z } from 'zod'
237
+
238
+ const WidgetSchema = z.object({ id: z.string(), name: z.string() })
239
+
240
+ const listWidgets = apiContract
241
+ .route({ method: 'GET', path: '/widgets', operationId: 'widget.list', tags: ['widget'] })
242
+ .input(PaginationInputSchema.extend({ keyword: z.string().optional() }))
243
+ .output(apiContract.haiResultSchema(apiContract.paginatedSchema(WidgetSchema)))
244
+ ```
245
+
246
+ ### 路径参数
247
+
248
+ ```typescript
249
+ // 路径中的 {id} 对应 input 中的 id 字段
250
+ const getWidget = apiContract
251
+ .route({ method: 'GET', path: '/widgets/{id}', operationId: 'widget.getById', tags: ['widget'] })
252
+ .input(z.object({ id: z.string() }))
253
+ .output(apiContract.haiResultSchema(WidgetSchema))
254
+ ```
255
+
256
+ ### 无输入的端点
257
+
258
+ ```typescript
259
+ const healthContract = apiContract
260
+ .route({ method: 'GET', path: '/health', operationId: 'health.check', tags: ['system'] })
261
+ .output(apiContract.haiResultSchema(z.object({ status: z.string() })))
262
+ ```
263
+
264
+ ---
265
+
266
+ ## 测试
267
+
268
+ ```bash
269
+ pnpm --filter @h-ai/api-contract test
270
+ pnpm --filter @h-ai/api-contract typecheck
271
+ pnpm --filter @h-ai/api-contract lint
272
+ ```
@@ -33,16 +33,17 @@ description: 应用开发入口技能。提供项目架构总览(SSR/SPA/原
33
33
  | 单元测试 | Vitest |
34
34
  | E2E 测试 | Playwright |
35
35
  | 原生 App | Capacitor 7 |
36
- | 认证 | Bearer Token(统一) |
36
+ | 公共 API | oRPC contract + Hono + typed client |
37
+ | 认证 | Bearer access token + httpOnly refresh cookie(浏览器推荐) |
37
38
 
38
39
  ### 多端构建模式
39
40
 
40
41
  | 模式 | Adapter | 认证方式 | 部署目标 |
41
42
  | ------- | -------------- | --------------------- | ----------------------- |
42
- | SSR Web | adapter-node | Bearer Token + Cookie | Node.js 服务器 |
43
- | SPA | adapter-static | Bearer Token | CDN / 静态托管 |
44
- | Android | adapter-static | Bearer Token | Capacitor → Android APK |
45
- | iOS | adapter-static | Bearer Token | Capacitor → iOS IPA |
43
+ | SSR Web | adapter-node | Bearer access token + Cookie | Node.js 服务器 |
44
+ | SPA | adapter-static | Bearer access token + httpOnly refresh cookie | CDN / 静态托管 |
45
+ | Android | adapter-static | Bearer access token + Capacitor secure storage | Capacitor → Android APK |
46
+ | iOS | adapter-static | Bearer access token + Capacitor secure storage | Capacitor → iOS IPA |
46
47
 
47
48
  **Adapter 切换**:
48
49
 
@@ -76,10 +77,11 @@ const config = {
76
77
  app.d.ts # 全局类型声明
77
78
  hooks.server.ts # 服务端 Hook(模块初始化 + 请求管道)
78
79
  lib/
79
- api.ts # API Client 初始化(SPA / 原生 App 使用)
80
+ api.ts # @h-ai/api-client 初始化(SPA / 原生 App 使用)
80
81
  capacitor.ts # Capacitor 初始化(原生 App)
81
82
  server/
82
83
  init.ts # 模块初始化入口(单例,SSR)
84
+ procedures/ # 应用私有 oRPC procedures(仅 API Service 场景)
83
85
  paraglide/ # i18n 生成文件(禁止手动修改)
84
86
  components/ # 应用组件
85
87
  routes/
@@ -105,8 +107,10 @@ core(基础能力:配置、日志、i18n、HaiResult)
105
107
  ├── ai(AI:LLM/MCP/Agent)
106
108
  ├── iam(身份管理)← 依赖 crypto + db + cache
107
109
  ├── payment(支付)← 依赖 db + crypto
108
- ├── kit(SvelteKit 集成)← 依赖 iam + cache + storage + crypto
109
- ├── api-client(HTTP 客户端)← 纯浏览器端,依赖 iam/api(类型)
110
+ ├── api-contract(公共 HTTP API 契约)← 纯定义,依赖 core + zod + oRPC contract
111
+ ├── serv(Hono + oRPC API Service)← 依赖 api-contract;features 按需依赖 iam/storage/ai
112
+ ├── kit(SvelteKit 集成)← Hook / guard / validate / response,不定义公共 contract
113
+ ├── api-client(typed API 客户端)← 依赖 api-contract + oRPC client
110
114
  ├── capacitor(原生能力)← 纯浏览器端
111
115
  └── ui(UI 组件库)← 依赖 core
112
116
  ```
@@ -144,6 +148,8 @@ export async function initModules() {
144
148
 
145
149
  // 4. 集成层
146
150
  // kit 无需 init,通过 createHandle 配置
151
+ // api-contract 无需 init,只在服务端/客户端共享 contract
152
+ // serv 在 API Service 中通过 serv.createApp({ contract, procedures }) 装配
147
153
 
148
154
  initialized = true
149
155
  }
@@ -152,16 +158,17 @@ export async function initModules() {
152
158
  #### SPA / 原生 App 模式(src/lib/api.ts)
153
159
 
154
160
  ```typescript
155
- import { api } from '@h-ai/api-client'
161
+ import { apiClient } from '@h-ai/api-client'
156
162
 
157
163
  export async function initApi() {
158
- return api.init({
164
+ return apiClient.init({
159
165
  baseUrl: import.meta.env.VITE_API_BASE_URL,
166
+ auth: {}, // 浏览器默认 httpOnly cookie;原生 App 传入 Capacitor TokenStorage
160
167
  timeout: 15_000,
161
168
  })
162
169
  }
163
170
 
164
- export { api }
171
+ export { apiClient }
165
172
  ```
166
173
 
167
174
  #### 原生 App 追加初始化(src/lib/capacitor.ts)
@@ -204,9 +211,11 @@ log:
204
211
  | 加密/签名/哈希 | `hai-crypto` | crypto.init, SM2, SM3, SM4, 加密, 签名 |
205
212
  | 身份认证/授权 | `hai-iam` | iam.init, 登录, 注册, RBAC, Token, Bearer |
206
213
  | AI/LLM/MCP | `hai-ai` | ai.init, LLM, MCP, Agent, 工具调用 |
207
- | SvelteKit 集成 | `hai-kit` | kit.createHandle, guard, fromContract, middleware |
214
+ | 公共 API 契约 | `hai-api-contract` | apiContract.create, contract, schema, oRPC, HaiResult |
215
+ | API Service 运行时 | `hai-serv` | serv.createApp, Hono, procedures, OpenAPI, docs, requireAuth |
216
+ | SvelteKit 集成 | `hai-kit` | kit.createHandle, guard, middleware, validate, response |
208
217
  | UI 组件 | `hai-ui` | 表单, 按钮, 表格, Modal, Toast, 移动端组件 |
209
- | HTTP 客户端 | `hai-api-client` | api.call, api.get, api.post, Bearer, 拦截器 |
218
+ | typed API 客户端 | `hai-api-client` | apiClient.init, typed client, Bearer, 401 refresh, custom fetch |
210
219
  | 原生 App 能力 | `hai-capacitor` | capacitor, 相机, 推送, 状态栏, 设备信息 |
211
220
  | 支付 | `hai-payment` | payment, 微信支付, 支付宝, Stripe, 订单 |
212
221
 
@@ -217,6 +226,9 @@ log:
217
226
  | 创建页面/组件/API/模型 | `hai-app-create` | 新建页面, 添加组件, API 端点, 数据模型 |
218
227
  | 代码审查与规范化 | `hai-app-review` | 审查, review, 规范, 优化 |
219
228
  | 编写/补充测试 | `hai-app-tests` | 测试, test, 单测, 覆盖率, TDD, E2E |
229
+ | CI/CD 与质量门禁 | `hai-ci` | CI, CD, GitHub Actions, workflow, secret scan |
230
+ | PR / Issue 交付审查 | `hai-pr-review` | PR, pull request, issue, CODEOWNERS, 自动 Review, AI Review |
231
+ | hai-framework 同步 | `hai-framework-sync` | hai-framework, skills 同步, framework:use:local, framework:watch |
220
232
 
221
233
  ---
222
234
 
@@ -52,14 +52,14 @@ await capacitor.close()
52
52
  ### 2. Token 安全存储(与 api-client 配合)
53
53
 
54
54
  ```typescript
55
- import { api } from '@h-ai/api-client'
55
+ import { apiClient } from '@h-ai/api-client'
56
56
  import { createCapacitorTokenStorage } from '@h-ai/capacitor'
57
57
 
58
- await api.init({
58
+ await apiClient.init({
59
59
  baseUrl: import.meta.env.PUBLIC_API_BASE,
60
60
  auth: {
61
61
  storage: createCapacitorTokenStorage(),
62
- refreshUrl: '/api/v1/auth/refresh',
62
+ refreshPath: '/api/v1/auth/refresh',
63
63
  },
64
64
  })
65
65
  ```
@@ -278,20 +278,20 @@ const config = {
278
278
 
279
279
  ```typescript
280
280
  // src/lib/api.ts
281
- import { api } from '@h-ai/api-client'
281
+ import { apiClient } from '@h-ai/api-client'
282
282
  import { createCapacitorTokenStorage } from '@h-ai/capacitor'
283
283
 
284
284
  export async function initApi() {
285
- return api.init({
285
+ return apiClient.init({
286
286
  baseUrl: `${import.meta.env.PUBLIC_API_BASE}/api/v1`,
287
287
  auth: {
288
288
  storage: createCapacitorTokenStorage(),
289
- refreshUrl: '/auth/refresh',
289
+ refreshPath: '/auth/refresh',
290
290
  },
291
291
  })
292
292
  }
293
293
 
294
- export { api }
294
+ export { apiClient }
295
295
  ```
296
296
 
297
297
  ### 推送通知完整流程
@@ -0,0 +1,112 @@
1
+ ---
2
+ name: hai-ci
3
+ description: 设计和维护 hai-framework 应用仓库的 CI/CD、GitHub Actions、质量门禁、secret scan 与 workflow 安全;当需求涉及 CI、GitHub Actions、required checks、泄漏扫描或 release workflow 编排时使用。
4
+ ---
5
+
6
+ # hai-ci — CI/CD 与质量门禁规范
7
+
8
+ > 面向 AI 助手和团队成员的仓库自动化指南。目标是让每个 PR 都能被机器稳定验证,同时避免把 secrets 暴露给不可信 PR 或第三方 Action。
9
+
10
+ ---
11
+
12
+ ## 适用场景
13
+
14
+ - 新增或修改 `.github/workflows/**`
15
+ - 配置 `pnpm typecheck`、`pnpm lint`、`pnpm test`、`pnpm build`、E2E
16
+ - 调整 secret scan、依赖扫描、安全扫描、release workflow 编排
17
+ - 设计 required checks 的 CI 状态、workflow 权限、触发条件与 concurrency
18
+ - 排查 CI flaky、缓存、矩阵构建或执行顺序问题
19
+
20
+ 不负责:PR 人工审查流程(用 `hai-pr-review`)、测试用例设计(用 `hai-app-tests`)、代码质量审查(用 `hai-app-review`)、云资源开通与应用部署 API(用 `hai-deploy`)。
21
+
22
+ ---
23
+
24
+ ## 核心原则
25
+
26
+ 1. **确定性检查优先**:CI 只负责可重复的机器检查;测试内容设计交给 `hai-app-tests`,代码规范判断交给 `hai-app-review`。
27
+ 2. **最小权限**:默认 `permissions: contents: read`,只有 release、comment、pages 等明确场景才提升权限。
28
+ 3. **PR 不使用 secrets**:来自 fork 或不可信分支的 PR 不得运行读取 secrets 的 workflow。
29
+ 4. **禁用高风险组合**:禁止 `pull_request_target` + checkout PR 代码 + secrets/写权限。
30
+ 5. **先轻后重**:核心 CI 必须稳定;E2E、发布、安全深扫可以拆成 manual/nightly 或独立 workflow。
31
+ 6. **可追溯**:workflow 改动必须说明触发条件、权限、缓存、失败路径和验证命令。
32
+
33
+ ---
34
+
35
+ ## 标准 CI 工作流
36
+
37
+ ### PR / main 基础质量门禁
38
+
39
+ 建议顺序:
40
+
41
+ ```bash
42
+ pnpm install --frozen-lockfile
43
+ pnpm typecheck
44
+ pnpm lint
45
+ pnpm test
46
+ pnpm build
47
+ ```
48
+
49
+ 测试覆盖范围与 TDD 断言由 `hai-app-tests` 定义;本技能只决定这些命令如何在 workflow 中稳定执行。
50
+
51
+ ### E2E 策略
52
+
53
+ - 若 E2E 稳定且耗时可控,可作为 required check。
54
+ - 若 E2E 依赖浏览器、服务、密钥或容易 flaky,先放到 `workflow_dispatch` / `schedule` / 独立 job。
55
+ - 应用级 E2E 使用:`pnpm --filter <app> test:e2e`。
56
+
57
+ ### hai-framework 依赖策略
58
+
59
+ - CI 默认使用 `pnpm-lock.yaml` 中的 npm/published 依赖,确保与真实安装环境一致。
60
+ - 本地联调使用 `framework:use:local` / `framework:watch`,但不要让默认 PR CI 隐式依赖本机路径。
61
+ - 需要验证本地 framework 改动时,创建显式的 sync/drift check 或双仓联动 PR。
62
+
63
+ ---
64
+
65
+ ## GitHub Actions 安全清单
66
+
67
+ - [ ] workflow 顶层声明 `permissions`
68
+ - [ ] PR workflow 使用 `pull_request`,不用 `pull_request_target`
69
+ - [ ] 不在 PR workflow 中读取 secrets
70
+ - [ ] 第三方 action 固定版本;高风险 action 优先改为本地脚本或官方 action
71
+ - [ ] 使用 `concurrency` 取消同分支旧任务
72
+ - [ ] 不在 CI 日志打印 token、cookie、连接串、`.env` 内容
73
+ - [ ] release workflow 独立提权,只在 main/tag/manual 触发
74
+ - [ ] 失败日志可定位,不吞错、不 `|| true` 掩盖核心质量门禁
75
+
76
+ ---
77
+
78
+ ## Secret scan 策略
79
+
80
+ ### 推荐
81
+
82
+ - 用无 secrets 的高置信正则扫描常见密钥形态。
83
+ - 排除示例 skill、锁文件、图片等容易误报的路径。
84
+ - 需要私有关键词表时,仅在受信任分支的独立 workflow 中运行,不对 fork PR 暴露。
85
+
86
+ ### 禁止
87
+
88
+ - ❌ `pull_request_target` 中 checkout 不可信 PR 代码后读取 `secrets.*`
89
+ - ❌ 把完整密钥关键词列表传给未知第三方 action
90
+ - ❌ 为了通过扫描而删除失败证据或扩大忽略范围
91
+
92
+ ---
93
+
94
+ ## Workflow 变更交付清单
95
+
96
+ 修改 CI/CD 时,PR 描述必须包含:
97
+
98
+ - 触发条件:`pull_request` / `push` / `workflow_dispatch` / `schedule`
99
+ - 权限:顶层和 job 级 `permissions`
100
+ - 执行命令:每个 job 的核心命令
101
+ - 安全影响:是否读取 secrets、是否写仓库、是否评论 PR
102
+ - 验证方式:本地命令或 GitHub Actions dry run 结果
103
+
104
+ ---
105
+
106
+ ## 相关 Skills
107
+
108
+ - `hai-build` — 项目结构与质量门禁总览
109
+ - `hai-app-tests` — Vitest / Playwright 测试规范
110
+ - `hai-pr-review` — PR 审查与 AI Review 策略
111
+ - `hai-deploy` — 应用部署与云服务开通
112
+ - `hai-framework-sync` — hai-framework 与应用仓库同步策略
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: hai-core
3
- description: 使用 @h-ai/core 进行配置加载、日志记录、i18n 国际化、HaiResult 错误处理与模块生命周期管理;当需求涉及 core.init、core.logger、core.config、core.i18n、ok/err 或模块初始化模式时使用。
3
+ description: 使用 @h-ai/core 进行配置加载、日志记录、i18n 国际化、HaiResult 错误处理、Zod 校验错误本地化与模块生命周期管理;当需求涉及 core.init、core.logger、core.config、core.i18n、core.zodValidation、ok/err 或模块初始化模式时使用。
4
4
  ---
5
5
 
6
6
  # hai-core
7
7
 
8
- > `@h-ai/core` 是 hai-framework 的基础模块,提供配置管理、结构化日志、国际化、HaiResult 错误模型与模块生命周期工具。所有其他模块均依赖 core。
8
+ > `@h-ai/core` 是 hai-framework 的基础模块,提供配置管理、结构化日志、国际化、HaiResult 错误模型、Zod 校验错误本地化与模块生命周期工具。所有其他模块均依赖 core。
9
9
 
10
10
  ---
11
11
 
@@ -18,6 +18,7 @@ description: 使用 @h-ai/core 进行配置加载、日志记录、i18n 国际
18
18
  | `core.logger` | ✅ pino(结构化 JSON / pretty) | ✅ loglevel(DevTools console) |
19
19
  | `core.config` | ✅ 完整(YAML 加载、watch、validate) | ⚠️ 仅支持 `core.init({ ... })` 传入配置 |
20
20
  | `core.i18n` | ✅ | ✅ |
21
+ | `core.zodValidation` | ✅ | ✅ |
21
22
  | `core.id` | ✅ | ✅ |
22
23
  | 工具函数 | ✅ | ✅ |
23
24
 
@@ -29,6 +30,7 @@ description: 使用 @h-ai/core 进行配置加载、日志记录、i18n 国际
29
30
  - 使用 `core.logger` 记录日志
30
31
  - 使用 `core.config` 读取/校验模块配置
31
32
  - 使用 `core.i18n` 管理多语言
33
+ - 使用 `core.zodValidation` 将 Zod 默认英文错误映射为模块自己的 i18n 文案
32
34
  - 使用 `ok()` / `err()` 构建 HaiResult 类型返回值
33
35
  - 创建模块级 logger 或 i18n 消息获取器
34
36
 
@@ -56,6 +58,9 @@ core.logger.info('Server started', { port: 3000 })
56
58
 
57
59
  // 配置
58
60
  const dbConfig = core.config.get('db')
61
+
62
+ // Zod 校验错误本地化
63
+ const formErrors = core.zodValidation.mapZodErrorToFormErrors(zodError, getMessage)
59
64
  ```
60
65
 
61
66
  ---
@@ -210,6 +215,64 @@ m('user_created') // "用户已创建"
210
215
  m('welcome', { params: { name: '张三' } }) // "欢迎,张三"
211
216
  ```
212
217
 
218
+ ### Zod 校验错误映射 — `core.zodValidation`
219
+
220
+ 适用于 `@h-ai/kit`、`@h-ai/serv`、`@h-ai/api-client` 等需要把 Zod 默认英文错误
221
+ 转换为模块自身 i18n 文案的场景。优先复用 `core.zodValidation`,不要在各模块重复维护
222
+ 默认消息正则 / issue 本地化逻辑。
223
+
224
+ | 方法 | 签名 | 说明 |
225
+ |------|------|------|
226
+ | `createPrefixedZodMessageGetter` | `(prefix, getMessage) => ZodMessageGetter` | 按模块前缀自动派生 `serv_validation*` / `kit_validation*` 这类消息 key |
227
+ | `mapZodErrorToFormErrors` | `(error, getMessage) => ValidationFormError[]` | 一步完成提取 + 本地化 + 扁平化 |
228
+
229
+ ```typescript
230
+ import type { ZodMessageGetter } from '@h-ai/core'
231
+ import { core } from '@h-ai/core'
232
+ import { z } from 'zod'
233
+
234
+ const LoginSchema = z.object({
235
+ email: z.string().email(),
236
+ password: z.string().min(8),
237
+ nickname: z.string().min(1, '请填写昵称'),
238
+ })
239
+
240
+ const getMessage: ZodMessageGetter = (key, params) => {
241
+ switch (key) {
242
+ case 'validationEmail':
243
+ return '请输入合法邮箱地址'
244
+ case 'validationStringMin':
245
+ return `至少输入 ${params?.min} 个字符`
246
+ default:
247
+ return '输入不合法'
248
+ }
249
+ }
250
+
251
+ const result = LoginSchema.safeParse({
252
+ email: 'bad',
253
+ password: '123',
254
+ nickname: '',
255
+ })
256
+
257
+ if (!result.success) {
258
+ const getMessage = core.zodValidation.createPrefixedZodMessageGetter(
259
+ 'serv',
260
+ (messageKey, params) => {
261
+ if (messageKey === 'serv_validationEmail')
262
+ return '请输入合法邮箱地址'
263
+ if (messageKey === 'serv_validationStringMin')
264
+ return `至少输入 ${params?.min} 个字符`
265
+ return '输入不合法'
266
+ },
267
+ )
268
+ const errors = core.zodValidation.mapZodErrorToFormErrors(result.error, getMessage)
269
+
270
+ errors[0]?.field // 'email'
271
+ errors[0]?.message // '请输入合法邮箱地址'
272
+ errors[2]?.message // '请填写昵称'(schema 自定义消息原样保留)
273
+ }
274
+ ```
275
+
213
276
  ### ID 生成 — `core.id`
214
277
 
215
278
  | 方法 | 签名 | 说明 |