@h-ai/cli 0.1.0-alpha5
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/LICENSE +202 -0
- package/README.md +167 -0
- package/dist/chunk-2H7UOFLK.js +10 -0
- package/dist/chunk-2H7UOFLK.js.map +1 -0
- package/dist/chunk-LANIDJC7.js +659 -0
- package/dist/chunk-LANIDJC7.js.map +1 -0
- package/dist/chunk-YV7WPUI6.js +206 -0
- package/dist/chunk-YV7WPUI6.js.map +1 -0
- package/dist/config-templates-S5RI2MC6.js +5 -0
- package/dist/config-templates-S5RI2MC6.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1645 -0
- package/dist/index.js.map +1 -0
- package/dist/skill-templates-N5DU5FNX.js +5 -0
- package/dist/skill-templates-N5DU5FNX.js.map +1 -0
- package/dist/src-XJ6YXNAF.js +15030 -0
- package/dist/src-XJ6YXNAF.js.map +1 -0
- package/package.json +59 -0
- package/templates/README.md +126 -0
- package/templates/apps/admin/messages/en-US.json +33 -0
- package/templates/apps/admin/messages/zh-CN.json +33 -0
- package/templates/apps/admin/src/routes/+layout.svelte.hbs +30 -0
- package/templates/apps/admin/src/routes/+page.svelte.hbs +35 -0
- package/templates/apps/admin/src/routes/admin/+layout.svelte.hbs +44 -0
- package/templates/apps/admin/src/routes/admin/+page.svelte.hbs +45 -0
- package/templates/apps/admin/src/routes/admin/settings/+page.svelte.hbs +37 -0
- package/templates/apps/admin/src/routes/api/health/+server.ts +12 -0
- package/templates/apps/android-app/.env.hbs +3 -0
- package/templates/apps/android-app/capacitor.config.ts.hbs +12 -0
- package/templates/apps/android-app/messages/en-US.json +24 -0
- package/templates/apps/android-app/messages/zh-CN.json +24 -0
- package/templates/apps/android-app/src/lib/api.ts.hbs +30 -0
- package/templates/apps/android-app/src/lib/capacitor.ts +21 -0
- package/templates/apps/android-app/src/routes/+layout.svelte.hbs +42 -0
- package/templates/apps/android-app/src/routes/+layout.ts +7 -0
- package/templates/apps/android-app/src/routes/+page.svelte.hbs +27 -0
- package/templates/apps/api/src/routes/+layout.svelte +14 -0
- package/templates/apps/api/src/routes/+page.svelte +11 -0
- package/templates/apps/api/src/routes/api/v1/health/+server.ts +14 -0
- package/templates/apps/api/src/routes/api/v1/items/+server.ts +37 -0
- package/templates/apps/api/src/routes/api/v1/items/[id]/+server.ts +30 -0
- package/templates/apps/h5/messages/en-US.json +37 -0
- package/templates/apps/h5/messages/zh-CN.json +37 -0
- package/templates/apps/h5/src/routes/+layout.svelte.hbs +57 -0
- package/templates/apps/h5/src/routes/+page.svelte.hbs +27 -0
- package/templates/apps/h5/src/routes/discover/+page.svelte.hbs +27 -0
- package/templates/apps/h5/src/routes/profile/+page.svelte.hbs +27 -0
- package/templates/apps/website/messages/en-US.json +32 -0
- package/templates/apps/website/messages/zh-CN.json +32 -0
- package/templates/apps/website/src/routes/+layout.svelte.hbs +66 -0
- package/templates/apps/website/src/routes/+page.svelte.hbs +33 -0
- package/templates/apps/website/src/routes/about/+page.svelte.hbs +27 -0
- package/templates/apps/website/src/routes/api/health/+server.ts +12 -0
- package/templates/apps/website/src/routes/contact/+page.svelte.hbs +59 -0
- package/templates/apps/website/src/routes/services/+page.svelte.hbs +63 -0
- package/templates/base/e2e/helpers.ts +49 -0
- package/templates/base/package.json.hbs +67 -0
- package/templates/base/playwright.config.ts +32 -0
- package/templates/base/src/app.css +2 -0
- package/templates/base/src/app.d.ts.hbs +18 -0
- package/templates/base/src/app.html.hbs +16 -0
- package/templates/base/src/hooks.server.ts.hbs +123 -0
- package/templates/base/src/lib/server/init.ts.hbs +190 -0
- package/templates/base/svelte.config.js.hbs +30 -0
- package/templates/base/tsconfig.json +8 -0
- package/templates/base/vite.config.ts.hbs +27 -0
- package/templates/features/ai/routes/api/chat/+server.ts +26 -0
- package/templates/features/datapipe/routes/api/datapipe/pipeline/+server.ts +41 -0
- package/templates/features/iam/routes-admin/(auth)/+layout.svelte.hbs +21 -0
- package/templates/features/iam/routes-admin/(auth)/auth/login/+page.svelte.hbs +98 -0
- package/templates/features/iam/routes-admin/(auth)/auth/register/+page.svelte.hbs +124 -0
- package/templates/features/iam/routes-h5/auth/login/+page.svelte.hbs +98 -0
- package/templates/features/iam/routes-h5/auth/register/+page.svelte.hbs +114 -0
- package/templates/features/iam/routes-shared/api/auth/login/+server.ts +32 -0
- package/templates/features/iam/routes-shared/api/auth/logout/+server.ts +15 -0
- package/templates/features/iam/routes-shared/api/auth/me/+server.ts +12 -0
- package/templates/features/iam/routes-shared/api/auth/register/+server.ts +26 -0
- package/templates/features/payment/routes/api/v1/payment/create/+server.ts +30 -0
- package/templates/features/payment/routes/api/v1/payment/notify/[provider]/+server.ts +42 -0
- package/templates/features/payment/routes/api/v1/payment/query/[orderNo]/+server.ts +25 -0
- package/templates/features/storage/routes/api/upload/+server.ts +35 -0
- package/templates/features/vecdb/routes/api/vecdb/search/+server.ts +36 -0
- package/templates/i18n/project.inlang/settings.json +11 -0
- package/templates/skills/AGENTS.md +26 -0
- package/templates/skills/CLAUDE.md +12 -0
- package/templates/skills/copilot-instructions.md +61 -0
- package/templates/skills/hai-ai/SKILL.md +659 -0
- package/templates/skills/hai-api-client/SKILL.md +346 -0
- package/templates/skills/hai-app-create/SKILL.md +371 -0
- package/templates/skills/hai-app-review/SKILL.md +156 -0
- package/templates/skills/hai-app-tests/SKILL.md +468 -0
- package/templates/skills/hai-audit/SKILL.md +141 -0
- package/templates/skills/hai-build/SKILL.md +319 -0
- package/templates/skills/hai-cache/SKILL.md +184 -0
- package/templates/skills/hai-capacitor/SKILL.md +389 -0
- package/templates/skills/hai-core/SKILL.md +544 -0
- package/templates/skills/hai-crypto/SKILL.md +183 -0
- package/templates/skills/hai-datapipe/SKILL.md +214 -0
- package/templates/skills/hai-deploy/SKILL.md +210 -0
- package/templates/skills/hai-iam/SKILL.md +355 -0
- package/templates/skills/hai-kit/SKILL.md +319 -0
- package/templates/skills/hai-payment/SKILL.md +289 -0
- package/templates/skills/hai-reach/SKILL.md +325 -0
- package/templates/skills/hai-reldb/SKILL.md +459 -0
- package/templates/skills/hai-scheduler/SKILL.md +242 -0
- package/templates/skills/hai-storage/SKILL.md +230 -0
- package/templates/skills/hai-ui/SKILL.md +275 -0
- package/templates/skills/hai-vecdb/SKILL.md +273 -0
- package/templates/skills/opencode.json +11 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hai-reldb
|
|
3
|
+
description: 使用 @h-ai/reldb 进行 SQLite/PostgreSQL/MySQL 的初始化、SQL/DDL/CRUD/事务与分页操作;当需求涉及数据库访问、CRUD 仓库、事务处理、分页查询或 HaiReldbError 分支处理时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# hai-reldb
|
|
7
|
+
|
|
8
|
+
> `@h-ai/reldb` 提供统一的数据库操作接口,支持 SQLite、PostgreSQL、MySQL,包含 DDL、SQL、CRUD 抽象、事务与分页。
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 适用场景
|
|
13
|
+
|
|
14
|
+
- 新增或修改数据库访问逻辑(SQL/DDL/CRUD/事务)
|
|
15
|
+
- 使用 `reldb.crud.table` 或 `BaseReldbCrudRepository` 构建数据仓库
|
|
16
|
+
- 处理分页查询与分页结果规范化
|
|
17
|
+
- 基于 `HaiReldbError` 做错误分支处理
|
|
18
|
+
- JSON 列的路径提取、设置、插入、删除、合并操作(跨数据库统一语法)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## 使用步骤
|
|
23
|
+
|
|
24
|
+
### 1. 配置
|
|
25
|
+
|
|
26
|
+
```yaml
|
|
27
|
+
# config/_db.yml
|
|
28
|
+
type: ${HAI_RELDB_TYPE:sqlite}
|
|
29
|
+
database: ${HAI_RELDB_DATABASE:./data/app.db}
|
|
30
|
+
# PostgreSQL/MySQL 额外字段:
|
|
31
|
+
# host: ${HAI_RELDB_HOST:localhost}
|
|
32
|
+
# port: ${HAI_RELDB_PORT:5432}
|
|
33
|
+
# user: ${HAI_RELDB_USER:postgres}
|
|
34
|
+
# password: ${HAI_RELDB_PASSWORD:}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. 初始化与关闭
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { core } from '@h-ai/core'
|
|
41
|
+
import { reldb } from '@h-ai/reldb'
|
|
42
|
+
|
|
43
|
+
await reldb.init(core.config.get('db'))
|
|
44
|
+
// ... 使用数据库
|
|
45
|
+
await reldb.close()
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 3. 选择操作接口
|
|
49
|
+
|
|
50
|
+
| 接口 | 用途 | 入口 |
|
|
51
|
+
| ---- | ------------------ | ---------------------------- |
|
|
52
|
+
| DDL | 建表/索引/字段变更 | `reldb.ddl` |
|
|
53
|
+
| SQL | 原始查询/执行/分页 | `reldb.sql` |
|
|
54
|
+
| CRUD | 通用增删改查 | `reldb.crud.table(config)` |
|
|
55
|
+
| 仓库 | 业务数据仓库封装 | `extends BaseReldbCrudRepository` |
|
|
56
|
+
| 事务 | 事务管理 | `reldb.tx` |
|
|
57
|
+
| 分页 | 分页参数与结果 | `reldb.pagination` |
|
|
58
|
+
| JSON | JSON 路径操作 | `reldb.json` |
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 核心 API
|
|
63
|
+
|
|
64
|
+
### DDL — `reldb.ddl`
|
|
65
|
+
|
|
66
|
+
| 方法 | 签名 | 说明 |
|
|
67
|
+
| ------------- | ----------------------------------------------------------------------- | -------------------------- |
|
|
68
|
+
| `createTable` | `(tableName, columns: TableDef, ifNotExists?: boolean) => HaiResult<void>` | 建表(默认 IF NOT EXISTS) |
|
|
69
|
+
| `dropTable` | `(tableName, ifExists?: boolean) => HaiResult<void>` | 删除表(默认 IF EXISTS) |
|
|
70
|
+
| `addColumn` | `(tableName, columnName, columnDef: ColumnDef) => HaiResult<void>` | 添加列 |
|
|
71
|
+
| `dropColumn` | `(tableName, columnName) => HaiResult<void>` | 删除列 |
|
|
72
|
+
| `renameTable` | `(oldName, newName) => HaiResult<void>` | 重命名表 |
|
|
73
|
+
| `createIndex` | `(tableName, indexName, indexDef: IndexDef) => HaiResult<void>` | 创建索引 |
|
|
74
|
+
| `dropIndex` | `(indexName, ifExists?: boolean) => HaiResult<void>` | 删除索引 |
|
|
75
|
+
| `raw` | `(sql: string) => HaiResult<void>` | 执行原始 DDL |
|
|
76
|
+
|
|
77
|
+
> 所有方法均返回 `Promise<HaiResult<void>>`,上表省略异步与错误类型。
|
|
78
|
+
|
|
79
|
+
**TableDef**(列名到列定义的映射):
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
const columns: TableDef = {
|
|
83
|
+
id: { type: 'INTEGER', primaryKey: true, autoIncrement: true },
|
|
84
|
+
name: { type: 'TEXT', notNull: true },
|
|
85
|
+
email: { type: 'TEXT', unique: true },
|
|
86
|
+
score: { type: 'REAL', defaultValue: 0 },
|
|
87
|
+
active: { type: 'BOOLEAN' },
|
|
88
|
+
metadata: { type: 'JSON' },
|
|
89
|
+
created_at: { type: 'TIMESTAMP' },
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**ColumnDef**(列类型与约束):
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
interface ColumnDef {
|
|
97
|
+
type: 'TEXT' | 'INTEGER' | 'REAL' | 'BLOB' | 'BOOLEAN' | 'TIMESTAMP' | 'JSON'
|
|
98
|
+
primaryKey?: boolean
|
|
99
|
+
autoIncrement?: boolean
|
|
100
|
+
notNull?: boolean
|
|
101
|
+
unique?: boolean
|
|
102
|
+
defaultValue?: string | number | boolean | null
|
|
103
|
+
references?: { table: string, column: string, onDelete?: 'CASCADE' | 'SET NULL' | 'RESTRICT' | 'NO ACTION', onUpdate?: '...' }
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**跨数据库类型映射**:
|
|
108
|
+
|
|
109
|
+
| 类型 | SQLite | PostgreSQL | MySQL |
|
|
110
|
+
| --------- | ------- | ---------------- | ------------ |
|
|
111
|
+
| TEXT | TEXT | TEXT | VARCHAR(255) |
|
|
112
|
+
| INTEGER | INTEGER | INTEGER/SERIAL | INT/BIGINT |
|
|
113
|
+
| REAL | REAL | DOUBLE PRECISION | DOUBLE |
|
|
114
|
+
| BLOB | BLOB | BYTEA | BLOB |
|
|
115
|
+
| BOOLEAN | INTEGER | BOOLEAN | TINYINT(1) |
|
|
116
|
+
| TIMESTAMP | INTEGER | TIMESTAMP | DATETIME |
|
|
117
|
+
| JSON | TEXT | JSONB | JSON |
|
|
118
|
+
|
|
119
|
+
> MySQL 将 TEXT 映射为 VARCHAR(255) 以支持索引和 UNIQUE 约束。INTEGER + autoIncrement 在 MySQL 映射为 BIGINT。
|
|
120
|
+
|
|
121
|
+
### SQL — `reldb.sql`
|
|
122
|
+
|
|
123
|
+
| 方法 | 签名 | 说明 |
|
|
124
|
+
| ----------- | ----------------------------------------------------------------- | ------------ |
|
|
125
|
+
| `query` | `<T>(sql, params?) => HaiResult<T[]>` | 查询返回多行 |
|
|
126
|
+
| `get` | `<T>(sql, params?) => HaiResult<T \| null>` | 查询返回单行 |
|
|
127
|
+
| `execute` | `(sql, params?) => HaiResult<ExecuteResult>` | 执行写操作 |
|
|
128
|
+
| `batch` | `(statements: Array<{ sql, params? }>) => HaiResult<void>` | 批量执行 |
|
|
129
|
+
| `queryPage` | `<T>(options: PaginationQueryOptions) => HaiResult<PaginatedResult>` | 分页查询 |
|
|
130
|
+
|
|
131
|
+
> 所有方法均返回 `Promise<HaiResult<T>>`,上表省略异步与错误类型。
|
|
132
|
+
|
|
133
|
+
**参数占位符统一使用 `?`**(PostgreSQL 的 `$n` 由 Provider 自动转换):
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
const result = await reldb.sql.query<User>(
|
|
137
|
+
'SELECT * FROM users WHERE status = ? AND age > ?',
|
|
138
|
+
['active', 18],
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**ExecuteResult**:
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
interface ExecuteResult {
|
|
146
|
+
changes: number // 影响的行数
|
|
147
|
+
lastInsertRowid?: number | bigint // INSERT 时生效(PostgreSQL 不返回此字段)
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
**分页查询**:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
const page = await reldb.sql.queryPage<User>({
|
|
155
|
+
sql: 'SELECT * FROM users WHERE status = ? ORDER BY id',
|
|
156
|
+
params: ['active'],
|
|
157
|
+
pagination: { page: 1, pageSize: 20 },
|
|
158
|
+
overrides: { maxPageSize: 100 }, // 可选:限制最大页大小
|
|
159
|
+
})
|
|
160
|
+
// page.data => { items, total, page, pageSize }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### CRUD — `reldb.crud.table(config)`
|
|
164
|
+
|
|
165
|
+
通过配置创建单表 CRUD 仓库,免写 SQL:
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
const userCrud = reldb.crud.table<{ id: number, name: string, email: string }>({
|
|
169
|
+
table: 'users',
|
|
170
|
+
idColumn: 'id', // 主键列,默认 'id'
|
|
171
|
+
select: ['id', 'name', 'email'], // 查询列,默认 '*'
|
|
172
|
+
createColumns: ['name', 'email'], // 允许插入的列
|
|
173
|
+
updateColumns: ['name', 'email'], // 允许更新的列
|
|
174
|
+
})
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
返回的 ReldbCrudRepository 方法:
|
|
178
|
+
|
|
179
|
+
| 方法 | 签名 | 说明 |
|
|
180
|
+
| ------------ | ---------------------------------------------- | ------------ |
|
|
181
|
+
| `create` | `(data, tx?) => HaiResult<ExecuteResult>` | 创建单条 |
|
|
182
|
+
| `createMany` | `(items, tx?) => HaiResult<void>` | 批量创建 |
|
|
183
|
+
| `findById` | `(id, tx?) => HaiResult<T \| null>` | 按 ID 查询 |
|
|
184
|
+
| `findAll` | `(options?, tx?) => HaiResult<T[]>` | 条件查询 |
|
|
185
|
+
| `findPage` | `(options, tx?) => HaiResult<PaginatedResult<T>>` | 分页查询 |
|
|
186
|
+
| `updateById` | `(id, data, tx?) => HaiResult<ExecuteResult>` | 按 ID 更新 |
|
|
187
|
+
| `deleteById` | `(id, tx?) => HaiResult<ExecuteResult>` | 按 ID 删除 |
|
|
188
|
+
| `count` | `(options?, tx?) => HaiResult<number>` | 计数 |
|
|
189
|
+
| `exists` | `(options?, tx?) => HaiResult<boolean>` | 条件是否存在 |
|
|
190
|
+
| `existsById` | `(id, tx?) => HaiResult<boolean>` | ID 是否存在 |
|
|
191
|
+
|
|
192
|
+
> 所有方法均支持可选 `tx` 事务参数。`create`/`updateById` 中的 `data` 会根据 `createColumns`/`updateColumns` 白名单过滤列。
|
|
193
|
+
|
|
194
|
+
**使用示例**:
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
// 单条插入
|
|
198
|
+
const result = await userCrud.create({ name: '张三', email: 'test@example.com' })
|
|
199
|
+
// result.data → { changes: 1, lastInsertRowid: 1 }
|
|
200
|
+
|
|
201
|
+
// 批量插入
|
|
202
|
+
await userCrud.createMany([
|
|
203
|
+
{ name: '用户A', email: 'a@test.com' },
|
|
204
|
+
{ name: '用户B', email: 'b@test.com' },
|
|
205
|
+
])
|
|
206
|
+
|
|
207
|
+
// 主键查找
|
|
208
|
+
const user = await userCrud.findById(1)
|
|
209
|
+
// user.data → { id: 1, name: '张三', email: 'test@example.com' } | null
|
|
210
|
+
|
|
211
|
+
// 条件查询(where + params 占位符)
|
|
212
|
+
const actives = await userCrud.findAll({
|
|
213
|
+
where: 'name LIKE ?',
|
|
214
|
+
params: ['%张%'],
|
|
215
|
+
orderBy: 'id DESC',
|
|
216
|
+
limit: 10,
|
|
217
|
+
offset: 0,
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
// 分页查询
|
|
221
|
+
const page = await userCrud.findPage({
|
|
222
|
+
where: 'name LIKE ?',
|
|
223
|
+
params: ['%张%'],
|
|
224
|
+
orderBy: 'id DESC',
|
|
225
|
+
pagination: { page: 1, pageSize: 20 },
|
|
226
|
+
})
|
|
227
|
+
// page.data → { items: [...], total: 100, page: 1, pageSize: 20 }
|
|
228
|
+
|
|
229
|
+
// 更新 / 删除
|
|
230
|
+
await userCrud.updateById(1, { name: '新名字' })
|
|
231
|
+
await userCrud.deleteById(1)
|
|
232
|
+
|
|
233
|
+
// 计数 / 存在性
|
|
234
|
+
const total = await userCrud.count({ where: 'name LIKE ?', params: ['%张%'] })
|
|
235
|
+
const has = await userCrud.exists({ where: 'email = ?', params: ['test@example.com'] })
|
|
236
|
+
const found = await userCrud.existsById(1)
|
|
237
|
+
|
|
238
|
+
// 事务中使用(所有方法均支持 tx 参数)
|
|
239
|
+
await reldb.tx.wrap(async (tx) => {
|
|
240
|
+
await userCrud.create({ name: '用户A', email: 'a@test.com' }, tx)
|
|
241
|
+
await userCrud.updateById(1, { name: '新名字' }, tx)
|
|
242
|
+
const user = await userCrud.findById(1, tx)
|
|
243
|
+
})
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### BaseReldbCrudRepository
|
|
247
|
+
|
|
248
|
+
业务仓库基类,提供字段映射、自动建表与类型转换能力:
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
import { BaseReldbCrudRepository } from '@h-ai/reldb'
|
|
252
|
+
|
|
253
|
+
interface User { id: number, name: string, email: string, createdAt: Date, updatedAt: Date }
|
|
254
|
+
|
|
255
|
+
class UserRepository extends BaseReldbCrudRepository<User> {
|
|
256
|
+
constructor() {
|
|
257
|
+
super(db, {
|
|
258
|
+
table: 'users',
|
|
259
|
+
idColumn: 'id',
|
|
260
|
+
fields: [
|
|
261
|
+
{ fieldName: 'id', columnName: 'id', def: { type: 'INTEGER', primaryKey: true, autoIncrement: true }, select: true, create: false, update: false },
|
|
262
|
+
{ fieldName: 'name', columnName: 'name', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
|
|
263
|
+
{ fieldName: 'email', columnName: 'email', def: { type: 'TEXT', notNull: true }, select: true, create: true, update: true },
|
|
264
|
+
{ fieldName: 'createdAt', columnName: 'created_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
|
|
265
|
+
{ fieldName: 'updatedAt', columnName: 'updated_at', def: { type: 'TIMESTAMP', notNull: true }, select: true, create: true, update: false },
|
|
266
|
+
],
|
|
267
|
+
})
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/** 自定义查询示例 */
|
|
271
|
+
async findByEmail(email: string, tx?: DmlWithTxOperations) {
|
|
272
|
+
return this.sql(tx).get<User>('SELECT * FROM users WHERE email = ?', [email])
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
**`this.sql(tx?)`**:返回 `DmlOperations`(`reldb.sql` 或传入的事务句柄),自动适配事务场景。
|
|
278
|
+
|
|
279
|
+
**自动能力**:
|
|
280
|
+
|
|
281
|
+
- `createdAt` / `updatedAt` 字段自动填充时间戳
|
|
282
|
+
- 主键生成(非 autoIncrement 主键默认使用 `crypto.randomUUID()`)
|
|
283
|
+
- BOOLEAN → 1/0(SQLite/MySQL)或 true/false(PostgreSQL)
|
|
284
|
+
- TIMESTAMP → 毫秒时间戳(SQLite)或 Date(PG/MySQL)
|
|
285
|
+
- JSON → 字符串序列化(SQLite/MySQL)或原生 JSONB(PG)
|
|
286
|
+
|
|
287
|
+
### 事务 — `reldb.tx`
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// 方式1:wrap(自动 commit/rollback)
|
|
291
|
+
const result = await reldb.tx.wrap(async (tx) => {
|
|
292
|
+
await tx.execute('INSERT INTO users (name) VALUES (?)', ['张三'])
|
|
293
|
+
await tx.execute('INSERT INTO logs (action) VALUES (?)', ['user_created'])
|
|
294
|
+
return 'done'
|
|
295
|
+
})
|
|
296
|
+
// result.data === 'done'(正常返回自动 commit)
|
|
297
|
+
// 回调抛异常 → 自动 rollback → result.success === false
|
|
298
|
+
|
|
299
|
+
// 方式2:手动管理
|
|
300
|
+
const txResult = await reldb.tx.begin()
|
|
301
|
+
if (txResult.success) {
|
|
302
|
+
const tx = txResult.data
|
|
303
|
+
try {
|
|
304
|
+
await tx.execute('INSERT INTO users (name) VALUES (?)', ['李四'])
|
|
305
|
+
await tx.commit()
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
await tx.rollback()
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
**事务内可用操作**(`DmlWithTxOperations` 继承 `DmlOperations`):
|
|
314
|
+
|
|
315
|
+
- `tx.query / tx.get / tx.execute / tx.batch / tx.queryPage`
|
|
316
|
+
- `tx.crud.table(config)` — 事务内的 CRUD 仓库
|
|
317
|
+
- `tx.commit() / tx.rollback()`
|
|
318
|
+
|
|
319
|
+
### 分页 — `reldb.pagination`
|
|
320
|
+
|
|
321
|
+
| 方法 | 签名 | 说明 |
|
|
322
|
+
| ----------- | ----------------------------------------------------- | -------------------------------------- |
|
|
323
|
+
| `normalize` | `(options?, overrides?) => NormalizedPagination` | 规范化分页参数(含 offset/limit 计算) |
|
|
324
|
+
| `build` | `<T>(items, total, pagination) => PaginatedResult<T>` | 构建分页结果对象 |
|
|
325
|
+
|
|
326
|
+
```typescript
|
|
327
|
+
// PaginatedResult 结构
|
|
328
|
+
interface PaginatedResult<T> {
|
|
329
|
+
items: T[]
|
|
330
|
+
total: number
|
|
331
|
+
page: number
|
|
332
|
+
pageSize: number
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 默认值:page=1, pageSize=20, maxPageSize=200
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
### JSON — `reldb.json`
|
|
341
|
+
|
|
342
|
+
通过 `reldb.json` 构建跨数据库统一的 JSON 路径操作 SQL 表达式,返回 `{ sql, params }` 可直接嵌入 `reldb.sql.*`。
|
|
343
|
+
|
|
344
|
+
路径格式遵循 SQL/JSON Path 标准,以 `$` 开头(如 `$.key`、`$.key.subkey`、`$[0]`)。
|
|
345
|
+
|
|
346
|
+
| 操作 | SQLite | PostgreSQL | MySQL |
|
|
347
|
+
| --------- | ------------------ | ------------------------ | ---------------------- |
|
|
348
|
+
| `extract` | `json_extract` | `#>` (text[]) | `JSON_EXTRACT` |
|
|
349
|
+
| `set` | `json_set` | `jsonb_set` | `JSON_SET` |
|
|
350
|
+
| `insert` | `json_insert` | `jsonb_insert` | `JSON_INSERT` |
|
|
351
|
+
| `remove` | `json_remove` | `#-` (text[]) | `JSON_REMOVE` |
|
|
352
|
+
| `merge` | `json_patch` | `|| ::jsonb` | `JSON_MERGE_PATCH` |
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// 提取 JSON 字段值(用于 WHERE 条件)
|
|
356
|
+
const { sql, params } = reldb.json.extract('settings', '$.theme')
|
|
357
|
+
const rows = await reldb.sql.query(
|
|
358
|
+
`SELECT * FROM users WHERE ${sql} = ?`,
|
|
359
|
+
[...params, '"dark"'],
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
// 设置 JSON 字段路径(创建或替换)
|
|
363
|
+
const { sql, params } = reldb.json.set('settings', '$.theme', 'dark')
|
|
364
|
+
await reldb.sql.execute(
|
|
365
|
+
`UPDATE users SET settings = ${sql} WHERE id = ?`,
|
|
366
|
+
[...params, userId],
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
// 删除 JSON 字段路径
|
|
370
|
+
const { sql, params } = reldb.json.remove('settings', '$.deprecated')
|
|
371
|
+
await reldb.sql.execute(`UPDATE users SET settings = ${sql} WHERE id = ?`, [...params, id])
|
|
372
|
+
|
|
373
|
+
// 合并 JSON 对象(RFC 7396:null 值表示删除对应键)
|
|
374
|
+
const { sql, params } = reldb.json.merge('profile', { bio: '新简介', avatar: null })
|
|
375
|
+
await reldb.sql.execute(`UPDATE users SET profile = ${sql} WHERE id = ?`, [...params, userId])
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
> `column` 参数为列名,**禁止传入用户输入**(开发者负责安全性)。
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## 错误码 — `HaiReldbError`
|
|
383
|
+
|
|
384
|
+
| 错误码 | code | 说明 |
|
|
385
|
+
|--------|------|------|
|
|
386
|
+
| `HaiReldbError.CONNECTION_FAILED` | `hai:reldb:001` | 数据库连接失败 |
|
|
387
|
+
| `HaiReldbError.QUERY_FAILED` | `hai:reldb:002` | 查询或执行失败 |
|
|
388
|
+
| `HaiReldbError.CONSTRAINT_VIOLATION` | `hai:reldb:003` | 约束违反 |
|
|
389
|
+
| `HaiReldbError.TRANSACTION_FAILED` | `hai:reldb:004` | 事务失败 |
|
|
390
|
+
| `HaiReldbError.MIGRATION_FAILED` | `hai:reldb:005` | 迁移失败 |
|
|
391
|
+
| `HaiReldbError.RECORD_NOT_FOUND` | `hai:reldb:006` | 记录不存在 |
|
|
392
|
+
| `HaiReldbError.DUPLICATE_ENTRY` | `hai:reldb:007` | 重复条目 |
|
|
393
|
+
| `HaiReldbError.DEADLOCK` | `hai:reldb:008` | 死锁 |
|
|
394
|
+
| `HaiReldbError.TIMEOUT` | `hai:reldb:009` | 超时 |
|
|
395
|
+
| `HaiReldbError.POOL_EXHAUSTED` | `hai:reldb:010` | 连接池耗尽 |
|
|
396
|
+
| `HaiReldbError.NOT_INITIALIZED` | `hai:reldb:011` | 数据库未初始化 |
|
|
397
|
+
| `HaiReldbError.DDL_FAILED` | `hai:reldb:012` | DDL 操作失败 |
|
|
398
|
+
| `HaiReldbError.UNSUPPORTED_TYPE` | `hai:reldb:013` | 不支持的数据库类型 |
|
|
399
|
+
| `HaiReldbError.CONFIG_ERROR` | `hai:reldb:014` | 配置错误 |
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## 常见模式
|
|
404
|
+
|
|
405
|
+
### API 端点中使用
|
|
406
|
+
|
|
407
|
+
```typescript
|
|
408
|
+
import { reldb, HaiReldbError } from '@h-ai/reldb'
|
|
409
|
+
import { kit } from '@h-ai/kit'
|
|
410
|
+
|
|
411
|
+
export async function GET(event) {
|
|
412
|
+
const { valid, data } = kit.validate.query(event.url, PageSchema)
|
|
413
|
+
if (!valid)
|
|
414
|
+
return kit.response.badRequest('Invalid params')
|
|
415
|
+
|
|
416
|
+
const result = await userCrud.findPage({
|
|
417
|
+
pagination: data,
|
|
418
|
+
orderBy: 'id ASC',
|
|
419
|
+
})
|
|
420
|
+
if (!result.success)
|
|
421
|
+
return kit.response.internalError()
|
|
422
|
+
|
|
423
|
+
return kit.response.ok(result.data)
|
|
424
|
+
}
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### 事务跨仓库
|
|
428
|
+
|
|
429
|
+
```typescript
|
|
430
|
+
const result = await reldb.tx.wrap(async (tx) => {
|
|
431
|
+
const user = await userRepo.create(userData, tx)
|
|
432
|
+
if (!user.success)
|
|
433
|
+
throw new Error(user.error.message)
|
|
434
|
+
|
|
435
|
+
await profileRepo.create({ userId: user.data.lastInsertRowid, ...profileData }, tx)
|
|
436
|
+
return user.data
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### BaseReldbCrudRepository 事务集成
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
const txResult = await reldb.tx.begin()
|
|
444
|
+
if (txResult.success) {
|
|
445
|
+
const tx = txResult.data
|
|
446
|
+
await userRepo.create({ name: '用户A', email: 'a@test.com' }, tx)
|
|
447
|
+
await userRepo.create({ name: '用户B', email: 'b@test.com' }, tx)
|
|
448
|
+
await tx.commit()
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
---
|
|
453
|
+
|
|
454
|
+
## 相关 Skills
|
|
455
|
+
|
|
456
|
+
- `hai-build`:模块初始化顺序
|
|
457
|
+
- `hai-core`:配置管理、HaiResult 模型
|
|
458
|
+
- `hai-iam`:IAM 模块内部使用 reldb 进行用户/角色/权限存储
|
|
459
|
+
- `hai-cache`:缓存穿透保护中配合 reldb 使用
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: hai-scheduler
|
|
3
|
+
description: 使用 @h-ai/scheduler 进行统一定时任务管理、任务持久化、手工触发、触发来源审计、全局生命周期回调与执行日志查询;当需求涉及 cron 调度、JS 函数字符串任务、API 任务、分布式锁或任务事件回调时使用。
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# hai-scheduler
|
|
7
|
+
|
|
8
|
+
> `@h-ai/scheduler` 使用统一任务模型管理 API / JS / Hook 三类执行路径,支持持久化、手工触发来源记录、分布式锁与全局生命周期回调。
|
|
9
|
+
|
|
10
|
+
## 依赖
|
|
11
|
+
|
|
12
|
+
| 模块 | 用途 | 是否必需 | 初始化要求 |
|
|
13
|
+
| --- | --- | --- | --- |
|
|
14
|
+
| `@h-ai/reldb` | 任务定义与执行日志持久化 | `enableDb: true` 时必需 | 需在 `scheduler.init()` 前初始化 |
|
|
15
|
+
| `@h-ai/cache` | 分布式锁 | 可选 | 初始化后自动启用 |
|
|
16
|
+
|
|
17
|
+
## 使用步骤
|
|
18
|
+
|
|
19
|
+
### 1. 初始化
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { cache } from '@h-ai/cache'
|
|
23
|
+
import { reldb } from '@h-ai/reldb'
|
|
24
|
+
import { scheduler } from '@h-ai/scheduler'
|
|
25
|
+
|
|
26
|
+
await reldb.init({ type: 'sqlite', database: './scheduler.db' })
|
|
27
|
+
await cache.init({ type: 'memory' })
|
|
28
|
+
|
|
29
|
+
await scheduler.init({
|
|
30
|
+
enableDb: true,
|
|
31
|
+
maxLogs: 1000,
|
|
32
|
+
retentionDays: 30,
|
|
33
|
+
hooks: {
|
|
34
|
+
onTaskStart(event) {
|
|
35
|
+
console.info('start', event.task.id, event.trigger)
|
|
36
|
+
},
|
|
37
|
+
onTaskInterrupted(event) {
|
|
38
|
+
console.warn('interrupted', event.task.id, event.reason)
|
|
39
|
+
},
|
|
40
|
+
onTaskFinish(event) {
|
|
41
|
+
console.info('finish', event.log.status)
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
})
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 注册任务
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
await scheduler.register({
|
|
51
|
+
id: 'health-check',
|
|
52
|
+
name: '健康检查',
|
|
53
|
+
description: '每 5 分钟巡检一次接口健康状态',
|
|
54
|
+
cron: '*/5 * * * *',
|
|
55
|
+
params: { channel: 'ops' },
|
|
56
|
+
retry: { maxAttempts: 3, backoffMs: [1000, 5000] },
|
|
57
|
+
handler: {
|
|
58
|
+
kind: 'api',
|
|
59
|
+
url: 'https://api.example.com/health',
|
|
60
|
+
method: 'GET',
|
|
61
|
+
},
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
await scheduler.register({
|
|
65
|
+
id: 'cleanup',
|
|
66
|
+
name: '清理过期数据',
|
|
67
|
+
description: '夜间单次清理任务',
|
|
68
|
+
cron: '0 2 * * *',
|
|
69
|
+
deleteAfterRun: true,
|
|
70
|
+
params: { source: 'nightly' },
|
|
71
|
+
handler: {
|
|
72
|
+
kind: 'js',
|
|
73
|
+
code: '(context) => ({ taskId: context.taskId, params: context.params })',
|
|
74
|
+
},
|
|
75
|
+
})
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 3. 使用全局 execute hook 处理无 handler 任务
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
await scheduler.setHooks({
|
|
82
|
+
async onTaskExecute(event) {
|
|
83
|
+
return { via: 'hook', source: event.context.trigger.source }
|
|
84
|
+
},
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
await scheduler.register({
|
|
88
|
+
id: 'hook-task',
|
|
89
|
+
name: 'Hook 任务',
|
|
90
|
+
cron: '* * * * *',
|
|
91
|
+
})
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 4. 手工触发与日志查询
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const result = await scheduler.trigger('cleanup', { source: 'admin-console' })
|
|
98
|
+
|
|
99
|
+
const logs = await scheduler.getLogs({
|
|
100
|
+
taskId: 'cleanup',
|
|
101
|
+
triggerType: 'manual',
|
|
102
|
+
triggerSource: 'admin-console',
|
|
103
|
+
startedAfter: Date.now() - 24 * 60 * 60 * 1000,
|
|
104
|
+
startedBefore: Date.now(),
|
|
105
|
+
pagination: { page: 1, pageSize: 20 },
|
|
106
|
+
})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 核心类型
|
|
110
|
+
|
|
111
|
+
### TaskDefinition
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
interface TaskDefinition {
|
|
115
|
+
id: string
|
|
116
|
+
name: string
|
|
117
|
+
description?: string
|
|
118
|
+
cron: string
|
|
119
|
+
enabled?: boolean
|
|
120
|
+
deleteAfterRun?: boolean
|
|
121
|
+
retry?: {
|
|
122
|
+
maxAttempts: number
|
|
123
|
+
backoffMs?: number[]
|
|
124
|
+
}
|
|
125
|
+
params?: Record<string, unknown>
|
|
126
|
+
handler?: ApiTaskConfig | JsTaskConfig
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Handler 配置
|
|
131
|
+
|
|
132
|
+
```ts
|
|
133
|
+
interface ApiTaskConfig {
|
|
134
|
+
kind: 'api'
|
|
135
|
+
url: string
|
|
136
|
+
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
|
|
137
|
+
headers?: Record<string, string>
|
|
138
|
+
body?: unknown
|
|
139
|
+
timeout?: number
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
interface JsTaskConfig {
|
|
143
|
+
kind: 'js'
|
|
144
|
+
code: string
|
|
145
|
+
timeout?: number
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### TaskExecutionLog
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
interface TaskExecutionLog {
|
|
153
|
+
id: number
|
|
154
|
+
taskId: string
|
|
155
|
+
taskName: string
|
|
156
|
+
taskType: 'api' | 'js' | 'hook'
|
|
157
|
+
triggerType: 'scheduled' | 'manual'
|
|
158
|
+
triggerSource: string | null
|
|
159
|
+
status: 'success' | 'failed' | 'interrupted'
|
|
160
|
+
result: string | null
|
|
161
|
+
error: string | null
|
|
162
|
+
startedAt: number
|
|
163
|
+
finishedAt: number
|
|
164
|
+
duration: number
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 核心 API
|
|
169
|
+
|
|
170
|
+
| 方法 / 属性 | 签名 | 说明 |
|
|
171
|
+
| --- | --- | --- |
|
|
172
|
+
| `init` | `(config?) => Promise<HaiResult<void>>` | 初始化调度器 |
|
|
173
|
+
| `register` | `(task) => Promise<HaiResult<void>>` | 注册统一任务模型 |
|
|
174
|
+
| `updateTask` | `(taskId, updates) => Promise<HaiResult<void>>` | 更新 cron / params / handler |
|
|
175
|
+
| `register` 扩展字段 | `description / deleteAfterRun / retry` | 支持任务描述、一次性任务、失败重试策略 |
|
|
176
|
+
| `trigger` | `(taskId, { source? }) => Promise<HaiResult<TaskExecutionLog>>` | 手工触发并记录来源 |
|
|
177
|
+
| `getLogs` | `(options?) => Promise<HaiResult<PaginatedResult<TaskExecutionLog>>>` | 支持按 trigger + startedAfter/startedBefore 过滤日志 |
|
|
178
|
+
| `setHooks` | `(hooks) => HaiResult<void>` | 设置全局生命周期回调 |
|
|
179
|
+
| `clearHooks` | `() => HaiResult<void>` | 清空全局生命周期回调 |
|
|
180
|
+
| `start / stop` | `() => HaiResult<void>` | 启动 / 停止调度 |
|
|
181
|
+
| `close` | `() => Promise<void>` | 关闭调度器 |
|
|
182
|
+
|
|
183
|
+
## 常见模式
|
|
184
|
+
|
|
185
|
+
### 持久化 JS 任务
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
await scheduler.init({ enableDb: true })
|
|
189
|
+
|
|
190
|
+
await scheduler.register({
|
|
191
|
+
id: 'persisted-js',
|
|
192
|
+
name: '持久化 JS 任务',
|
|
193
|
+
cron: '*/10 * * * *',
|
|
194
|
+
handler: {
|
|
195
|
+
kind: 'js',
|
|
196
|
+
code: '(context) => ({ taskId: context.taskId })',
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### 手工触发来源审计
|
|
202
|
+
|
|
203
|
+
```ts
|
|
204
|
+
await scheduler.trigger('cleanup', { source: 'admin-console' })
|
|
205
|
+
await scheduler.trigger('cleanup', { source: 'cli' })
|
|
206
|
+
|
|
207
|
+
const adminLogs = await scheduler.getLogs({ triggerSource: 'admin-console' })
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 分布式锁
|
|
211
|
+
|
|
212
|
+
```ts
|
|
213
|
+
await cache.init({ type: 'redis', host: 'localhost', port: 6379 })
|
|
214
|
+
await reldb.init({ type: 'sqlite', database: './scheduler.db' })
|
|
215
|
+
await scheduler.init({
|
|
216
|
+
enableDb: true,
|
|
217
|
+
maxLogs: 500,
|
|
218
|
+
retentionDays: 14,
|
|
219
|
+
lockExpireMs: 300000,
|
|
220
|
+
nodeId: 'node-1',
|
|
221
|
+
})
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## 错误码 — `HaiSchedulerError`
|
|
225
|
+
|
|
226
|
+
| 错误码 | code | 说明 |
|
|
227
|
+
|--------|------|------|
|
|
228
|
+
| `HaiSchedulerError.NOT_INITIALIZED` | `hai:scheduler:010` | 未初始化 |
|
|
229
|
+
| `HaiSchedulerError.INIT_FAILED` | `hai:scheduler:011` | 初始化失败 |
|
|
230
|
+
| `HaiSchedulerError.CONFIG_ERROR` | `hai:scheduler:012` | 配置错误 |
|
|
231
|
+
| `HaiSchedulerError.TASK_NOT_FOUND` | `hai:scheduler:020` | 任务不存在 |
|
|
232
|
+
| `HaiSchedulerError.TASK_ALREADY_EXISTS` | `hai:scheduler:021` | 任务已存在 |
|
|
233
|
+
| `HaiSchedulerError.INVALID_CRON` | `hai:scheduler:022` | Cron 表达式无效 |
|
|
234
|
+
| `HaiSchedulerError.EXECUTION_FAILED` | `hai:scheduler:023` | 执行失败 |
|
|
235
|
+
| `HaiSchedulerError.JS_EXECUTION_FAILED` | `hai:scheduler:024` | JS 执行失败 |
|
|
236
|
+
| `HaiSchedulerError.API_EXECUTION_FAILED` | `hai:scheduler:025` | API 执行失败 |
|
|
237
|
+
| `HaiSchedulerError.DB_SAVE_FAILED` | `hai:scheduler:026` | DB 保存失败 |
|
|
238
|
+
| `HaiSchedulerError.ALREADY_RUNNING` | `hai:scheduler:027` | 已在运行 |
|
|
239
|
+
| `HaiSchedulerError.NOT_RUNNING` | `hai:scheduler:028` | 未在运行 |
|
|
240
|
+
| `HaiSchedulerError.LOCK_ACQUIRE_FAILED` | `hai:scheduler:029` | 锁获取失败 |
|
|
241
|
+
| `HaiSchedulerError.JS_COMPILE_FAILED` | `hai:scheduler:030` | JS 编译失败 |
|
|
242
|
+
| `HaiSchedulerError.HOOK_EXECUTION_FAILED` | `hai:scheduler:031` | Hook 执行失败 |
|