zentao-api 0.5.2 → 0.5.4

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.md CHANGED
@@ -1,308 +1,378 @@
1
1
  # zentao-api
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/zentao-api)](https://www.npmjs.com/package/zentao-api)
4
- [![license](https://img.shields.io/npm/l/zentao-api)](./LICENSE)
5
- [![node](https://img.shields.io/node/v/zentao-api)](https://nodejs.org)
4
+ [![Node.js](https://img.shields.io/node/v/zentao-api)](https://nodejs.org)
5
+ [![license](https://img.shields.io/npm/l/zentao-api)](https://github.com/easysoft/zentao-api/blob/main/LICENSE)
6
6
 
7
7
  Browser & Node.js SDK for [ZenTao](https://www.zentao.net) (禅道) API v2.
8
8
 
9
- `zentao-api` 是一个面向禅道 API v2 的轻量 JavaScript/TypeScript SDK,可用于 Node.js 18+、浏览器打包工具以及 CDN/script 标签场景。
9
+ `zentao-api` 是一个零运行时依赖的 JavaScript/TypeScript SDK,提供底层 REST 客户端和基于模块注册表的高阶请求接口,可运行在 Node.js 18+、Bun、浏览器打包工具及 CDN/script 标签环境中。
10
10
 
11
- ---
11
+ [快速开始](#快速开始) · [调用方式](#两种调用方式) · [浏览器](#浏览器) · [完整文档](#文档)
12
12
 
13
- ## 安装 / Install
13
+ ## 特性
14
+
15
+ - 两层 API:使用 `ZentaoClient` 直接调用 REST 路径,或使用 `request("module/action")` 自动组装路径、查询参数和请求体。
16
+ - 完整类型提示:内置请求名、参数和 `data` 返回值可由 TypeScript 自动推导。
17
+ - 统一响应结构:自动提取业务数据和分页信息,稳定返回 `ResponseData<T>`。
18
+ - 覆盖禅道常用模块:产品、项目、执行、需求、任务、Bug、测试、版本、发布等。
19
+ - 内置本地数据处理:支持转换、过滤、搜索、排序、限制数量和字段摘取。
20
+ - 可扩展模块注册表、持久化 Profile、稳定错误码及 Node.js 自签名证书支持。
21
+
22
+ ## 安装
14
23
 
15
24
  ```sh
16
25
  npm install zentao-api
17
26
  ```
18
27
 
19
- ## 快速开始 / Quick Start
28
+ 使用 Bun:
29
+
30
+ ```sh
31
+ bun add zentao-api
32
+ ```
33
+
34
+ 包采用 ESM,并自带 TypeScript 类型定义。Node.js 需要 18 或更高版本。
20
35
 
21
- ### 创建客户端
36
+ ## 快速开始
37
+
38
+ 推荐通过 `ZentaoClient.init()` 配置全局客户端,再使用高阶 `request()` 调用内置模块:
22
39
 
23
40
  ```ts
24
- import { ZentaoClient } from 'zentao-api';
41
+ import { ZentaoClient, request } from 'zentao-api';
25
42
 
26
- const client = new ZentaoClient({
43
+ ZentaoClient.init({
27
44
  baseUrl: 'https://zentao.example.com',
28
45
  token: 'your-token',
29
46
  });
30
47
 
31
- const products = await client.get('/products');
48
+ const result = await request('product/list', {
49
+ browseType: 'all',
50
+ recPerPage: 20,
51
+ pageID: 1,
52
+ });
53
+
54
+ console.log(result.data); // 产品列表
55
+ console.log(result.pager?.total); // 总记录数
32
56
  ```
33
57
 
34
- `baseUrl` 是禅道站点根地址。SDK 会在内部追加 `/api.php/v2`。
58
+ `baseUrl` 填写禅道站点根地址;SDK 会自动拼接 `/api.php/v2`,并在后续请求中注入 `Token` 请求头。
35
59
 
36
- ### 账号密码登录
60
+ ### 使用账号密码登录
37
61
 
38
- 如果还没有 token,可以使用账号密码登录:
62
+ 没有 token 时,可以先登录。`ZentaoClient.init()` 返回的实例同时也是 `request()` 使用的全局客户端:
39
63
 
40
64
  ```ts
41
- const client = new ZentaoClient('https://zentao.example.com');
42
- const token = await client.login('admin', 'password');
43
- ```
65
+ import { ZentaoClient, request } from 'zentao-api';
44
66
 
45
- ### 全局客户端与模块请求
46
-
47
- ```ts
48
- import { ZentaoClient, request, setGlobalOptions } from 'zentao-api';
49
-
50
- ZentaoClient.init({
67
+ const client = ZentaoClient.init({
51
68
  baseUrl: 'https://zentao.example.com',
52
- token: 'your-token',
53
69
  });
54
70
 
55
- setGlobalOptions({ recPerPage: '50' });
71
+ await client.login('admin', 'password');
56
72
 
57
- const result = await request('product/list', {});
73
+ const products = await request('product/list');
58
74
  ```
59
75
 
76
+ 请从环境变量或安全配置中读取账号、密码和 token,不要将凭据提交到代码仓库。
77
+
78
+ ## 两种调用方式
79
+
80
+ | 调用方式 | 适合场景 | 返回值 |
81
+ | --- | --- | --- |
82
+ | `request("module/action")` | 调用注册表中的常用禅道 API,自动处理参数与分页 | 统一的 `ResponseData<T>` |
83
+ | `ZentaoClient` | 调用尚未注册的路径、上传文件或读取二进制响应 | 禅道原始响应体 |
84
+
85
+ ### 高阶模块请求
86
+
60
87
  请求名支持三种写法:
61
88
 
62
89
  ```ts
63
- await request('product'); // 默认调用 product/list
64
- await request('product/list'); // 显式调用模块动作
65
- await request('product/1'); // 详情快捷写法,等价于 product/get + id=1
90
+ await request('product'); // product/list
91
+ await request('product/list'); // 显式动作名
92
+ await request('product/1'); // product/get,且对象 ID 为 1
66
93
  ```
67
94
 
68
- 单次调用的选项会覆盖全局选项:
95
+ 带作用域的列表可以传产品、项目或执行 ID,SDK 会自动选择实际路径:
69
96
 
70
97
  ```ts
71
- const result = await request('bug/list', { product: 1 }, { limit: '10' });
72
- ```
98
+ const bugs = await request('bug/list', {
99
+ productID: 1,
100
+ browseType: 'unclosed',
101
+ });
73
102
 
74
- ### 从本地 Profile 恢复
103
+ // 也可以显式指定作用域:
104
+ const projectBugs = await request('bug/list', {
105
+ scope: 'projects',
106
+ scopeID: 8,
107
+ });
108
+ ```
75
109
 
76
- SDK 支持将登录信息持久化到本地(Node.js: `~/.config/zentao/zentao.json`,浏览器: `localStorage`),后续可直接恢复客户端:
110
+ 单次调用选项会覆盖全局选项。下面的处理只作用于 SDK 返回的 `data`,不会改变服务端数据:
77
111
 
78
112
  ```ts
79
- const client = await ZentaoClient.fromProfile();
80
- // 或指定 profile key
81
- const client = await ZentaoClient.fromProfile('admin@https://zentao.example.com');
113
+ const bugs = await request(
114
+ 'bug/list',
115
+ { productID: 1, recPerPage: 100 },
116
+ {
117
+ filter: ['status=active,pri>=2'],
118
+ search: ['登录'],
119
+ sort: 'pri:desc,id:asc',
120
+ limit: '10',
121
+ pick: ['id', 'title', 'pri'],
122
+ },
123
+ );
82
124
  ```
83
125
 
84
- ## API 概览
85
-
86
- ### ZentaoClient
87
-
88
- | 方法 | 说明 |
89
- |------|------|
90
- | `client.get<T>(path)` | GET 请求 |
91
- | `client.post<T>(path, body)` | POST 请求 |
92
- | `client.put<T>(path, body)` | PUT 请求 |
93
- | `client.delete<T>(path)` | DELETE 请求 |
94
- | `client.login(account, password)` | 账号密码登录,返回 token |
95
- | `client.request(path, options?)` | 通用请求(底层方法) |
96
- | `ZentaoClient.init(options)` | 创建全局单例客户端 |
97
- | `ZentaoClient.create(options)` | 工厂方法创建客户端 |
98
- | `ZentaoClient.fromProfile(key?)` | 从持久化 profile 恢复客户端 |
99
-
100
- ### 模块请求
101
-
102
- | 函数 | 说明 |
103
- |------|------|
104
- | `request(name, params?, options?)` | 按 `"module"`、`"module/action"` 或 `"module/<objectID>"` 调用已注册模块 |
105
- | `defineModules(modules, options?)` | 注册或扩展模块定义 |
106
- | `defineModuleActions(module, actions)` | 为已有模块追加或替换动作 |
107
- | `extendModuleAction(module, action, patch)` | 深度合并补丁,或用回调改写已有动作 |
108
- | `getModule(name)` | 获取模块定义 |
109
- | `getModuleAction(module, action)` | 获取指定动作定义 |
110
- | `getModuleNames()` | 获取所有已注册模块名 |
111
- | `setGlobalOptions(options)` | 设置全局默认选项 |
112
- | `getGlobalOptions()` | 获取当前全局选项 |
126
+ 更多过滤语法和处理顺序见[本地数据处理指南](https://github.com/easysoft/zentao-api/blob/main/docs/guide/data-processing.md)。
113
127
 
114
- ## 错误处理
128
+ ### 统一返回结构
115
129
 
116
- SDK 所有传输层错误均通过 `ZentaoError` 抛出,包含稳定的错误码:
130
+ 除非启用 `raw`,`request()` 始终返回以下结构:
117
131
 
118
132
  ```ts
119
- import { ZentaoError } from 'zentao-api';
120
-
121
- try {
122
- await client.get('/products');
123
- } catch (error) {
124
- if (error instanceof ZentaoError) {
125
- console.error(error.code); // e.g. 'E_HTTP_ERROR', 'E_TIMEOUT', 'E_NETWORK_ERROR'
126
- console.error(error.message);
127
- }
133
+ interface ResponseData<T> {
134
+ status: 'success' | 'fail';
135
+ message?: string;
136
+ data?: T;
137
+ pager?: {
138
+ total: number;
139
+ page: number;
140
+ recPerPage: number;
141
+ };
128
142
  }
129
143
  ```
130
144
 
131
- > **注意**:服务端返回 `{ status: "fail" }` 时 SDK 默认不会抛出异常,按原始响应内容返回。需要把业务失败转为异常时,可在单次请求或全局选项中启用 `throwOnFail`。HTTP/网络/超时等传输层错误始终会抛出 `ZentaoError`。
145
+ 内置请求会自动推导参数和数据类型;自定义调用也可以显式收窄 `data`:
132
146
 
133
- ## 扩展模块
147
+ ```ts
148
+ interface ProductSummary {
149
+ id: number;
150
+ name: string;
151
+ }
134
152
 
135
- 生成的模块定义来自 `scripts/update-registry.ts`。你可以在调用 `request()` 前扩展模块,或新增、替换动作。
153
+ const result = await request<ProductSummary[]>('product/list', {});
154
+ result.data?.forEach((product) => console.log(product.name));
155
+ ```
136
156
 
137
- ### 新增模块
157
+ 需要完整服务端响应时,传入 `{ raw: true }`。此时会跳过响应归一化、本地数据处理和 `throwOnFail`:
138
158
 
139
159
  ```ts
140
- import { defineModules } from 'zentao-api';
141
-
142
- defineModules({
143
- name: 'custom',
144
- actions: [
145
- {
146
- name: 'list',
147
- type: 'list',
148
- method: 'GET',
149
- path: '/custom',
150
- resultType: 'list',
151
- resultGetter: 'items',
152
- },
153
- ],
154
- });
160
+ const raw = await request('product/list', {}, { raw: true });
155
161
  ```
156
162
 
157
- ### 为已有模块追加动作
163
+ ### 底层 REST 客户端
164
+
165
+ `ZentaoClient` 适合直接调用 API v2 路径:
158
166
 
159
167
  ```ts
160
- import { defineModuleActions } from 'zentao-api';
161
-
162
- defineModuleActions('bug', {
163
- name: 'archive',
164
- type: 'action',
165
- method: 'PUT',
166
- path: '/bugs/{bugID}/archive',
167
- pathParams: { bugID: 'Bug ID' },
168
- resultType: 'text',
168
+ import { ZentaoClient } from 'zentao-api';
169
+
170
+ const client = new ZentaoClient({
171
+ baseUrl: 'https://zentao.example.com',
172
+ token: 'your-token',
173
+ timeout: 10_000,
169
174
  });
175
+
176
+ const products = await client.get('/products');
177
+ const product = await client.get('/products/1');
178
+ const created = await client.post('/products', { name: '新产品' });
170
179
  ```
171
180
 
172
- ### 微调已有动作
181
+ 通用 `client.request()` 还支持自定义请求头、查询参数、`AbortSignal`、`FormData`,以及 `text`、`arrayBuffer`、`blob`、`response` 等响应类型。
173
182
 
174
- 只想改动作的个别字段(而不是整体替换)时,用 `extendModuleAction`。传入补丁对象会与原动作**深度合并**:普通对象递归合并,数组及其他值整体替换,`undefined` 的键会被忽略。
183
+ ## 配置
175
184
 
176
- ```ts
177
- import { extendModuleAction } from 'zentao-api';
185
+ ### 客户端选项
178
186
 
179
- // 改写 task/list 的 URL
180
- extendModuleAction('task', 'list', {
181
- path: '/executions/{executionID}/tasks',
182
- pathParams: { executionID: '执行ID' },
187
+ | 选项 | 类型 | 说明 |
188
+ | --- | --- | --- |
189
+ | `baseUrl` | `string` | 禅道站点根地址;SDK 自动处理 `/api.php/v2`。 |
190
+ | `token` | `string` | 禅道 API Token;也可稍后通过 `login()` 获取。 |
191
+ | `timeout` | `number` | 默认请求超时时间,单位为毫秒,默认 `10000`。 |
192
+ | `insecure` | `boolean` | 跳过 TLS 证书校验,仅支持 Node.js 运行时。 |
193
+
194
+ ### 全局选项
195
+
196
+ ```ts
197
+ import { setGlobalOptions } from 'zentao-api';
198
+
199
+ setGlobalOptions({
200
+ recPerPage: '50',
201
+ limit: '20',
202
+ timeout: 30_000,
203
+ throwOnFail: true,
204
+ autoFill: false,
183
205
  });
184
206
  ```
185
207
 
186
- 需要基于现有定义做条件改写时,可传入回调。回调收到当前动作的深克隆,返回值作为**完整**动作定义直接取代原动作(不再合并):
208
+ 常用全局选项包括 `client`、`recPerPage`、`limit`、`timeout`、`insecure`、`persistProfiles`、`throwOnFail` 和 `autoFill`。优先级通常为:单次调用选项 > 全局选项 > 客户端默认值。
209
+
210
+ ### 持久化 Profile
211
+
212
+ Profile 默认不会写入。先启用 `persistProfiles`,登录成功后才会保存站点、账号、token 和客户端配置:
187
213
 
188
214
  ```ts
189
- extendModuleAction('execution', 'create', (action) => {
190
- const required = action.requestBody!.schema?.required;
191
- if (Array.isArray(required) && !required.includes('products')) {
192
- required.push('products');
193
- }
194
- return action;
215
+ import { ZentaoClient, setGlobalOptions } from 'zentao-api';
216
+
217
+ setGlobalOptions({ persistProfiles: true });
218
+
219
+ const client = ZentaoClient.init({
220
+ baseUrl: 'https://zentao.example.com',
195
221
  });
196
- ```
197
222
 
198
- ### 合并与替换
223
+ await client.login('admin', 'password');
224
+ ```
199
225
 
200
- 同名模块默认**合并**定义:同名动作会替换,未知动作会追加。如需**整体替换**模块,传入 `{ replace: true }`:
226
+ 后续可以恢复当前 Profile;如需继续调用高阶 `request()`,再把恢复的客户端设为全局客户端:
201
227
 
202
228
  ```ts
203
- defineModules(myModule, { replace: true });
229
+ const client = await ZentaoClient.fromProfile();
230
+ setGlobalOptions({ client });
231
+
232
+ // 或指定 profile key
233
+ const another = await ZentaoClient.fromProfile(
234
+ 'admin@https://zentao.example.com',
235
+ );
204
236
  ```
205
237
 
206
- 如果扩展定义拆分在多个文件中,请在应用启动入口中显式导入这些文件,确保它们在调用 `request()` 前完成注册。
238
+ | 环境 | 存储位置 |
239
+ | --- | --- |
240
+ | Node.js / Bun | `~/.config/zentao/zentao.json` |
241
+ | 浏览器 | `localStorage` |
207
242
 
208
- ## TypeScript 支持
243
+ Profile 包含可直接调用 API 的 token,请按敏感凭据保护其存储位置。
209
244
 
210
- SDK 提供完整的 TypeScript 类型定义,所有公共类型均可直接导入:
245
+ ## 错误处理
246
+
247
+ HTTP、网络、超时、参数、模块解析和 Profile 错误会统一抛出带稳定错误码的 `ZentaoError`:
211
248
 
212
249
  ```ts
213
- import type {
214
- ZentaoClientOptions,
215
- ModuleDefinition,
216
- ModuleAction,
217
- ResponseData,
218
- RequestOptions,
219
- } from 'zentao-api';
250
+ import { request, ZentaoError } from 'zentao-api';
251
+
252
+ try {
253
+ await request('bug/resolve', {
254
+ bugID: 1001,
255
+ resolution: 'fixed',
256
+ }, {
257
+ throwOnFail: true,
258
+ });
259
+ } catch (error) {
260
+ if (error instanceof ZentaoError) {
261
+ console.error(error.code); // 例如 E_HTTP_ERROR、E_TIMEOUT
262
+ console.error(error.message);
263
+ console.error(error.details);
264
+ }
265
+ }
220
266
  ```
221
267
 
268
+ 禅道返回 `{ status: "fail" }` 属于业务失败,默认仍作为 `ResponseData` 返回;只有启用单次或全局 `throwOnFail` 后,才会抛出 `E_API_FAILED`。HTTP、网络和超时等传输层错误始终抛出异常。
269
+
222
270
  ## 浏览器
223
271
 
224
- 浏览器打包工具可以正常导入这个包:
272
+ Vite、Webpack、Rspack 等打包工具可以从包根导入;需要显式选择浏览器入口时使用 `zentao-api/browser`:
225
273
 
226
274
  ```ts
227
- import { ZentaoClient } from 'zentao-api';
275
+ import { ZentaoClient, request } from 'zentao-api/browser';
228
276
  ```
229
277
 
230
- 如果使用 script 标签,请使用浏览器构建包,并从 `window.ZentaoAPI` 读取 API:
278
+ 使用 script 标签时,UMD 构建会将公共 API 暴露到 `window.ZentaoAPI`:
231
279
 
232
280
  ```html
233
281
  <script src="https://cdn.jsdelivr.net/npm/zentao-api@latest/dist/browser/zentao-api.global.js"></script>
234
282
  <script>
235
- console.log(window.ZentaoAPI.VERSION, window.ZentaoAPI.BUILD);
236
- const client = new window.ZentaoAPI.ZentaoClient('https://zentao.example.com');
283
+ const client = new window.ZentaoAPI.ZentaoClient({
284
+ baseUrl: 'https://zentao.example.com',
285
+ token: 'your-token',
286
+ });
287
+
288
+ console.log(window.ZentaoAPI.VERSION);
237
289
  </script>
238
290
  ```
239
291
 
240
- > **CORS**:浏览器直接请求要求禅道服务器允许 CORS。浏览器代码也会把 token 暴露给前端;如果这不可接受,请使用后端代理。
241
- >
242
- > **TLS**:`insecure` TLS 选项仅适用于 Node.js,在浏览器运行时会抛出错误。
292
+ > 浏览器直连要求禅道服务器允许 CORS,并会向前端暴露 token;敏感场景请通过后端代理。`insecure` 仅适用于 Node.js,在浏览器中使用会抛出 `E_INSECURE_BROWSER`。
243
293
 
244
- ## 测试
294
+ ## 模块注册表与扩展
245
295
 
246
- 本仓库开发依赖管理仅使用 [Bun](https://bun.sh)(`bun install`)。请勿使用 npm / pnpm / yarn,以免生成其它 lockfile。
296
+ 可以在运行时查看 SDK 当前支持的模块、动作和参数:
247
297
 
248
- ```sh
249
- bun test # 单元测试
250
- bun run test:coverage # 含覆盖率的单元测试
251
- bun run check # 完整 CI 流程:测试 + 类型检查 + 注册表 + 构建 + 冒烟测试
298
+ ```ts
299
+ import {
300
+ getModuleAction,
301
+ getModuleActionParams,
302
+ getModuleNames,
303
+ getObjectProps,
304
+ } from 'zentao-api';
305
+
306
+ const modules = getModuleNames();
307
+ const action = getModuleAction('bug', 'create');
308
+ const params = getModuleActionParams('bug', 'create');
309
+ const labels = getObjectProps('bug');
252
310
  ```
253
311
 
254
- ### 真实环境测试
312
+ 未注册的 API 可以新增为自定义模块:
255
313
 
256
- 真实环境测试需要连接到运行中的禅道实例,不包含在默认 `bun test` 中:
314
+ ```ts
315
+ import { defineModules } from 'zentao-api';
257
316
 
258
- ```sh
259
- bun run test:real
260
- bun run test:real -- --keep-test-data # 保留临时数据以便手动检查
317
+ defineModules({
318
+ name: 'custom',
319
+ actions: [
320
+ {
321
+ name: 'list',
322
+ type: 'list',
323
+ path: '/custom',
324
+ resultGetter: 'items',
325
+ },
326
+ ],
327
+ });
261
328
  ```
262
329
 
263
- 测试会优先读取 `.env.local`,如果不存在则读取 `env.local`。支持的环境变量:
264
-
265
- | 变量 | 说明 | 默认值 |
266
- |------|------|--------|
267
- | `ZENTAO_URL` | 禅道站点地址 | *(必填)* |
268
- | `ZENTAO_ACCOUNT` | 登录账号 | *(必填)* |
269
- | `ZENTAO_PASSWORD` | 登录密码 | *(必填)* |
270
- | `ZENTAO_TOKEN` | 直接提供 Token(替代账号密码) | — |
271
- | `ZENTAO_REVIEWER` | 需求评审人(未设置时复用 `ZENTAO_ACCOUNT`) | — |
272
- | `ZENTAO_KEEP_TEST_DATA` | 保留临时测试数据 | `false` |
273
- | `ZENTAO_TIMEOUT` | 请求超时(ms) | `30000` |
274
- | `ZENTAO_INSECURE` | 跳过 TLS 证书验证 | `false` |
330
+ 只需修改已有动作的个别字段时,使用 `extendModuleAction()`;补丁对象会深度合并,数组会整体替换:
275
331
 
276
- ## 项目结构
332
+ ```ts
333
+ import { extendModuleAction } from 'zentao-api';
277
334
 
335
+ extendModuleAction('task', 'list', {
336
+ path: '/executions/{executionID}/tasks',
337
+ pathParams: { executionID: '执行 ID' },
338
+ });
278
339
  ```
279
- zentao-api/
280
- ├── src/
281
- │ ├── client/ # ZentaoClient 核心实现
282
- │ ├── modules/ # 模块注册表与解析逻辑
283
- │ │ ├── generated.ts # 自动生成,勿手动编辑
284
- │ │ ├── registry.ts # 运行时注册表
285
- │ │ └── resolve.ts # 路径模板与参数解析
286
- │ ├── request/ # 高阶请求函数
287
- │ ├── profiles/ # 本地 profile 持久化
288
- │ ├── misc/ # 错误、全局选项、环境检测
289
- │ ├── types/ # TypeScript 类型定义
290
- │ ├── utils/ # 通用工具函数
291
- │ └── index.ts # 公共 API 入口
292
- ├── scripts/ # 构建与代码生成脚本
293
- ├── tests/ # 单元测试与真实环境测试
294
- ├── data/ # OpenAPI 规范文件
295
- └── dist/ # 构建产物
340
+
341
+ `defineModuleActions()` 可追加或整体替换单个动作,`defineModules(module, { replace: true })` 可整体替换同名模块。请确保扩展代码在第一次调用 `request()` 前执行。
342
+
343
+ ## 文档
344
+
345
+ - [快速开始](https://github.com/easysoft/zentao-api/blob/main/docs/guide/index.md)
346
+ - [安装与配置](https://github.com/easysoft/zentao-api/blob/main/docs/guide/installation.md)
347
+ - [常见 API 示例](https://github.com/easysoft/zentao-api/blob/main/docs/guide/examples.md)
348
+ - [Profile 与错误处理](https://github.com/easysoft/zentao-api/blob/main/docs/guide/profiles-and-errors.md)
349
+ - [SDK API Reference](https://github.com/easysoft/zentao-api/tree/main/docs/reference)
350
+ - [ZenTao 模块与动作列表](https://github.com/easysoft/zentao-api/tree/main/docs/zentao-api)
351
+ - [变更日志](https://github.com/easysoft/zentao-api/blob/main/CHANGES.md)
352
+
353
+ ## 开发与贡献
354
+
355
+ 本仓库只使用 [Bun](https://bun.sh) 管理开发依赖,请勿使用 npm、pnpm 或 yarn 安装仓库依赖,以免生成额外 lockfile。
356
+
357
+ ```sh
358
+ bun install
359
+ bun test # 单元测试
360
+ bun run test:real # 真实禅道环境集成测试
361
+ bun run docs:dev # 生成并预览文档站
362
+ bun run check # 完整 CI:测试、类型检查、注册表、构建、冒烟测试
296
363
  ```
297
364
 
298
- ## 贡献
365
+ `bun run test:real` 会依次读取 `.env.local` 和 `env.local`,需要配置 `ZENTAO_URL`(或 `ZENTAO_BASE_URL`),以及 `ZENTAO_TOKEN` 或 `ZENTAO_ACCOUNT` / `ZENTAO_PASSWORD`。使用 `bun run test:real -- --keep-test-data` 可保留测试创建的数据。
299
366
 
300
- 欢迎贡献代码!请确保提交前通过完整检查:
367
+ 模块注册表由 `data/zentao-openapi.json` 生成。请勿手动编辑 `src/modules/generated.ts`;更新规范后运行:
301
368
 
302
369
  ```sh
303
- bun run check
370
+ bun run scripts/update-registry.ts
371
+ bun run docs:generate
304
372
  ```
305
373
 
306
- ## 许可证 / License
374
+ 提交代码前请确保 `bun run check` 通过。欢迎提交 Issue 和 Pull Request。
375
+
376
+ ## License
307
377
 
308
- [MIT](./LICENSE)
378
+ [MIT](https://github.com/easysoft/zentao-api/blob/main/LICENSE)