zentao-api 0.2.0-beta.1 → 0.2.0

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.
@@ -1,37 +1,140 @@
1
1
  import type { ClientRequestOptions, ZentaoClientOptions } from '../types/index.js';
2
- /** 禅道 API 客户端,负责 Token 注入、请求超时、TLS 选项和响应解析。 */
2
+ /**
3
+ * 禅道 API 客户端,封装一次次原始 HTTP 调用。
4
+ *
5
+ * 主要职责:
6
+ * - 站点根地址规范化与 `/api.php/v2` 拼接
7
+ * - 自动注入 `Token` 头
8
+ * - 请求超时控制(基于 {@link AbortController})
9
+ * - 可选的 TLS 跳过校验(仅 Node.js 运行时)
10
+ * - 响应体的 JSON 解析与错误归一化
11
+ *
12
+ * 适合直接调用裸 API;若希望按模块/动作名调用并自动组装路径、参数、分页,
13
+ * 请改用 {@link request}。
14
+ */
3
15
  export declare class ZentaoClient {
4
16
  /** 禅道站点根地址,不包含 `/api.php/v2`。 */
5
17
  readonly siteUrl: string;
6
- /** 禅道 API v2 根地址。 */
18
+ /** 禅道 API v2 根地址,等于 `siteUrl + '/api.php/v2'`。 */
7
19
  readonly baseUrl: string;
8
20
  private token?;
9
21
  private readonly timeout?;
10
22
  private readonly insecure?;
11
- /** 使用完整配置创建客户端。 */
23
+ /**
24
+ * 使用完整配置创建客户端。
25
+ *
26
+ * @param options - 客户端配置,参见 {@link ZentaoClientOptions}。
27
+ * @throws {ZentaoError} `E_INVALID_BASE_URL` —— `baseUrl` 无法解析为合法的 http(s) URL。
28
+ */
12
29
  constructor(options: ZentaoClientOptions);
13
- /** 使用站点根地址创建客户端。 */
30
+ /**
31
+ * 使用站点根地址创建客户端。
32
+ *
33
+ * @param baseUrl - 禅道站点根地址,例如 `https://zentao.example.com`;
34
+ * 若误传 `/api.php/v2` 后缀会自动剥离。
35
+ * @throws {ZentaoError} `E_INVALID_BASE_URL` —— 地址不合法或协议非 http(s)。
36
+ */
14
37
  constructor(baseUrl: string);
15
38
  /**
16
39
  * 发起一次原始 API 请求。
17
40
  *
18
- * 默认使用 GET;当服务端返回 `{ status: "fail" }` 时仍按原始内容返回,
19
- * 只有 HTTP/网络/超时等传输层错误会抛出 {@link ZentaoError}。
41
+ * 选项优先级:本次调用 `options` > 全局选项({@link getGlobalOptions} > 客户端构造时默认值。
42
+ *
43
+ * 特殊处理:
44
+ * - 默认 HTTP 方法为 `GET`,`GET` 请求即使提供了 `options.body` 也不会发送,避免被部分代理/浏览器拒绝。
45
+ * - 非空响应优先按 JSON 解析;解析失败时回落为字符串原文。
46
+ * - 业务层失败(即响应体 `{ status: "fail" }`)不会抛出,仍按原样返回;只有 HTTP/网络/超时等传输层错误才会抛错。
47
+ * - `insecure` 仅在 Node.js 下可用,浏览器中传入会抛 `E_INSECURE_BROWSER`。
48
+ *
49
+ * @param path - 相对 {@link baseUrl} 的路径,可省略前导 `/`。
50
+ * @param options - 单次请求选项,参见 {@link ClientRequestOptions}。
51
+ * @returns 解析后的响应体;当响应为空字符串时返回 `undefined`。
52
+ * @throws {ZentaoError} 可能抛出 `E_HTTP_ERROR`(非 2xx 状态)、`E_NETWORK_ERROR`(底层 fetch 失败)、
53
+ * `E_TIMEOUT`(超过 `timeout`)或 `E_INSECURE_BROWSER`(浏览器中开启了 `insecure`)。
20
54
  */
21
55
  request(path: string, options?: ClientRequestOptions): Promise<unknown>;
22
- /** 发起 GET 请求并按调用方指定类型返回。 */
56
+ /**
57
+ * 发起 `GET` 请求。
58
+ *
59
+ * @typeParam T - 期望的响应体类型;调用方负责类型收窄,SDK 不做运行时校验。
60
+ * @param path - 相对 {@link baseUrl} 的路径。
61
+ * @returns 解析后的响应体(强转为 `T`)。
62
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
63
+ */
23
64
  get<T>(path: string): Promise<T>;
24
- /** 发起 POST 请求并发送 JSON body。 */
25
- post<T>(path: string, body: any): Promise<T>;
26
- /** 发起 PUT 请求并发送 JSON body。 */
27
- put<T>(path: string, body: any): Promise<T>;
28
- /** 发起 DELETE 请求。 */
65
+ /**
66
+ * 发起 `POST` 请求,`body` 会被序列化为 JSON。
67
+ *
68
+ * @typeParam T - 期望的响应体类型。
69
+ * @param path - 相对 {@link baseUrl} 的路径。
70
+ * @param body - JSON 请求体,传入对象/数组将被 `JSON.stringify`。
71
+ * @returns 解析后的响应体(强转为 `T`)。
72
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
73
+ */
74
+ post<T>(path: string, body: unknown): Promise<T>;
75
+ /**
76
+ * 发起 `PUT` 请求,`body` 会被序列化为 JSON。
77
+ *
78
+ * @typeParam T - 期望的响应体类型。
79
+ * @param path - 相对 {@link baseUrl} 的路径。
80
+ * @param body - JSON 请求体。
81
+ * @returns 解析后的响应体(强转为 `T`)。
82
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
83
+ */
84
+ put<T>(path: string, body: unknown): Promise<T>;
85
+ /**
86
+ * 发起 `DELETE` 请求。
87
+ *
88
+ * @typeParam T - 期望的响应体类型。
89
+ * @param path - 相对 {@link baseUrl} 的路径。
90
+ * @returns 解析后的响应体(强转为 `T`)。
91
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
92
+ */
29
93
  delete<T>(path: string): Promise<T>;
30
- /** 使用账号密码登录,成功后把返回 Token 写入当前客户端实例。 */
94
+ /**
95
+ * 使用账号密码登录禅道。
96
+ *
97
+ * 成功后会把返回的 Token 写入当前客户端实例(后续请求自动带上 `Token` 头);
98
+ * 当全局 `persistProfiles` 为真时,会同时把账号、Token、用户信息、服务端配置和
99
+ * 客户端偏好(仅在显式设置过 `timeout` / `insecure` 时)持久化为本地 profile,
100
+ * 并切换为当前 profile,方便下次通过 {@link ZentaoClient.fromProfile} 直接登录态恢复。
101
+ *
102
+ * @param account - 禅道用户账号。
103
+ * @param password - 禅道用户密码(明文,仅在传输层 TLS 内使用)。
104
+ * @returns 登录成功后返回的 API Token。
105
+ * @throws {ZentaoError} `E_LOGIN_FAILED` —— 服务端返回 `status !== "success"` 或缺失 token;
106
+ * 也可能因底层 {@link ZentaoClient.request} 而抛出 HTTP/网络/超时错误。
107
+ */
31
108
  login(account: string, password: string): Promise<string>;
32
- /** 创建客户端实例,语义等同于 `new ZentaoClient(options)`。 */
109
+ /**
110
+ * 创建客户端实例,语义等同于 `new ZentaoClient(options)`,便于链式调用。
111
+ *
112
+ * @param options - 客户端配置,参见 {@link ZentaoClientOptions}。
113
+ * @returns 新建的客户端实例。
114
+ * @throws {ZentaoError} 同 {@link ZentaoClient | 构造函数}:`E_INVALID_BASE_URL` 等。
115
+ */
33
116
  static create(options: ZentaoClientOptions): ZentaoClient;
34
- /** 创建客户端并写入全局选项,供高阶 `request()` 默认使用。 */
117
+ /**
118
+ * 创建客户端并写入全局选项,作为 {@link request} 默认使用的实例。
119
+ *
120
+ * 适合应用入口处一次性完成初始化,后续 `request("module/action", params)` 可省略 `options.client`。
121
+ * 多次调用会覆盖上一次的全局客户端。
122
+ *
123
+ * @param options - 客户端配置,参见 {@link ZentaoClientOptions}。
124
+ * @returns 新建并已注册为全局默认的客户端实例。
125
+ * @throws {ZentaoError} 同 {@link ZentaoClient | 构造函数}:`E_INVALID_BASE_URL` 等。
126
+ */
35
127
  static init(options: ZentaoClientOptions): ZentaoClient;
128
+ /**
129
+ * 根据本地持久化 profile 创建客户端。
130
+ *
131
+ * 实际会调用 {@link switchProfile}:若 `profileKey` 存在则刷新其 `lastUsedTime` 并设为当前 profile;
132
+ * 不传 `profileKey` 时使用当前 profile。Profile 中保存的 `timeout` / `insecure` 偏好也会被带回到客户端实例。
133
+ *
134
+ * @param profileKey - 可选的 profile key,格式为 `account@server`;不传时使用当前 profile。
135
+ * @returns 用 profile 还原后的客户端实例。
136
+ * @throws {ZentaoError} `E_NO_PROFILE`(无任何 profile 且未传 key)、`E_PROFILE_NOT_FOUND`(指定 key 不存在)、
137
+ * `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问持久化存储)。
138
+ */
139
+ static fromProfile(profileKey?: string): Promise<ZentaoClient>;
36
140
  }
37
- export declare function createClient(options: ZentaoClientOptions): ZentaoClient;
@@ -1,14 +1,9 @@
1
1
  import { ZentaoError } from '../misc/errors.js';
2
- import { assertInsecureSupported, withInsecureTls } from '../misc/environment.js';
2
+ import { assertInsecureSupported, fetchWithInsecureTls } from '../misc/environment.js';
3
3
  import { getGlobalOptions, setGlobalOptions } from '../misc/global-options.js';
4
+ import { addProfile, switchProfile } from '../profiles/index.js';
5
+ import { isRecord, normalizeSiteUrl } from '../utils/index.js';
4
6
  const DEFAULT_TIMEOUT = 10000;
5
- /** 将用户传入的站点根地址规范化,兼容误传入 `/api.php/v2` 的场景。 */
6
- function normalizeSiteUrl(baseUrl) {
7
- const trimmed = baseUrl.trim().replace(/\/+$/, '');
8
- if (!trimmed)
9
- throw new ZentaoError('E_INVALID_BASE_URL');
10
- return trimmed.replace(/\/api\.php\/v2$/i, '');
11
- }
12
7
  /** 拼接 API 路径与查询参数,跳过值为 `undefined` 的查询项。 */
13
8
  function buildUrl(baseUrl, path, query) {
14
9
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
@@ -32,11 +27,23 @@ async function parseResponse(response) {
32
27
  return text;
33
28
  }
34
29
  }
35
- /** 禅道 API 客户端,负责 Token 注入、请求超时、TLS 选项和响应解析。 */
30
+ /**
31
+ * 禅道 API 客户端,封装一次次原始 HTTP 调用。
32
+ *
33
+ * 主要职责:
34
+ * - 站点根地址规范化与 `/api.php/v2` 拼接
35
+ * - 自动注入 `Token` 头
36
+ * - 请求超时控制(基于 {@link AbortController})
37
+ * - 可选的 TLS 跳过校验(仅 Node.js 运行时)
38
+ * - 响应体的 JSON 解析与错误归一化
39
+ *
40
+ * 适合直接调用裸 API;若希望按模块/动作名调用并自动组装路径、参数、分页,
41
+ * 请改用 {@link request}。
42
+ */
36
43
  export class ZentaoClient {
37
44
  /** 禅道站点根地址,不包含 `/api.php/v2`。 */
38
45
  siteUrl;
39
- /** 禅道 API v2 根地址。 */
46
+ /** 禅道 API v2 根地址,等于 `siteUrl + '/api.php/v2'`。 */
40
47
  baseUrl;
41
48
  token;
42
49
  timeout;
@@ -52,8 +59,19 @@ export class ZentaoClient {
52
59
  /**
53
60
  * 发起一次原始 API 请求。
54
61
  *
55
- * 默认使用 GET;当服务端返回 `{ status: "fail" }` 时仍按原始内容返回,
56
- * 只有 HTTP/网络/超时等传输层错误会抛出 {@link ZentaoError}。
62
+ * 选项优先级:本次调用 `options` > 全局选项({@link getGlobalOptions} > 客户端构造时默认值。
63
+ *
64
+ * 特殊处理:
65
+ * - 默认 HTTP 方法为 `GET`,`GET` 请求即使提供了 `options.body` 也不会发送,避免被部分代理/浏览器拒绝。
66
+ * - 非空响应优先按 JSON 解析;解析失败时回落为字符串原文。
67
+ * - 业务层失败(即响应体 `{ status: "fail" }`)不会抛出,仍按原样返回;只有 HTTP/网络/超时等传输层错误才会抛错。
68
+ * - `insecure` 仅在 Node.js 下可用,浏览器中传入会抛 `E_INSECURE_BROWSER`。
69
+ *
70
+ * @param path - 相对 {@link baseUrl} 的路径,可省略前导 `/`。
71
+ * @param options - 单次请求选项,参见 {@link ClientRequestOptions}。
72
+ * @returns 解析后的响应体;当响应为空字符串时返回 `undefined`。
73
+ * @throws {ZentaoError} 可能抛出 `E_HTTP_ERROR`(非 2xx 状态)、`E_NETWORK_ERROR`(底层 fetch 失败)、
74
+ * `E_TIMEOUT`(超过 `timeout`)或 `E_INSECURE_BROWSER`(浏览器中开启了 `insecure`)。
57
75
  */
58
76
  async request(path, options = {}) {
59
77
  const globals = getGlobalOptions();
@@ -64,9 +82,7 @@ export class ZentaoClient {
64
82
  const url = buildUrl(this.baseUrl, path, options.query);
65
83
  const controller = new AbortController();
66
84
  const timer = setTimeout(() => controller.abort(), timeout);
67
- const headers = {
68
- 'Content-Type': 'application/json',
69
- };
85
+ const headers = {};
70
86
  if (this.token) {
71
87
  headers.Token = this.token;
72
88
  }
@@ -76,25 +92,24 @@ export class ZentaoClient {
76
92
  signal: controller.signal,
77
93
  };
78
94
  // GET 请求不携带 body,避免浏览器和部分代理拒绝请求。
79
- if (options.body && method !== 'GET') {
95
+ if (options.body !== undefined && method !== 'GET') {
96
+ headers['Content-Type'] = 'application/json';
80
97
  init.body = JSON.stringify(options.body);
81
98
  }
82
99
  try {
83
- return await withInsecureTls(insecure, async () => {
84
- const response = await fetch(url, init);
85
- if (!response.ok) {
86
- throw new ZentaoError('E_HTTP_ERROR', {
87
- status: response.status,
88
- statusText: response.statusText,
89
- }, {
90
- url: response.url,
91
- status: response.status,
92
- statusText: response.statusText,
93
- body: await response.text().catch(() => undefined),
94
- });
95
- }
96
- return parseResponse(response);
97
- });
100
+ const response = await fetchWithInsecureTls(insecure, url, init);
101
+ if (!response.ok) {
102
+ throw new ZentaoError('E_HTTP_ERROR', {
103
+ status: response.status,
104
+ statusText: response.statusText,
105
+ }, {
106
+ url: response.url,
107
+ status: response.status,
108
+ statusText: response.statusText,
109
+ body: await response.text().catch(() => undefined),
110
+ });
111
+ }
112
+ return parseResponse(response);
98
113
  }
99
114
  catch (error) {
100
115
  if (error instanceof ZentaoError)
@@ -108,42 +123,137 @@ export class ZentaoClient {
108
123
  clearTimeout(timer);
109
124
  }
110
125
  }
111
- /** 发起 GET 请求并按调用方指定类型返回。 */
126
+ /**
127
+ * 发起 `GET` 请求。
128
+ *
129
+ * @typeParam T - 期望的响应体类型;调用方负责类型收窄,SDK 不做运行时校验。
130
+ * @param path - 相对 {@link baseUrl} 的路径。
131
+ * @returns 解析后的响应体(强转为 `T`)。
132
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
133
+ */
112
134
  async get(path) {
113
135
  return this.request(path, { method: 'GET' });
114
136
  }
115
- /** 发起 POST 请求并发送 JSON body。 */
137
+ /**
138
+ * 发起 `POST` 请求,`body` 会被序列化为 JSON。
139
+ *
140
+ * @typeParam T - 期望的响应体类型。
141
+ * @param path - 相对 {@link baseUrl} 的路径。
142
+ * @param body - JSON 请求体,传入对象/数组将被 `JSON.stringify`。
143
+ * @returns 解析后的响应体(强转为 `T`)。
144
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
145
+ */
116
146
  async post(path, body) {
117
147
  return this.request(path, { method: 'POST', body });
118
148
  }
119
- /** 发起 PUT 请求并发送 JSON body。 */
149
+ /**
150
+ * 发起 `PUT` 请求,`body` 会被序列化为 JSON。
151
+ *
152
+ * @typeParam T - 期望的响应体类型。
153
+ * @param path - 相对 {@link baseUrl} 的路径。
154
+ * @param body - JSON 请求体。
155
+ * @returns 解析后的响应体(强转为 `T`)。
156
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
157
+ */
120
158
  async put(path, body) {
121
159
  return this.request(path, { method: 'PUT', body });
122
160
  }
123
- /** 发起 DELETE 请求。 */
161
+ /**
162
+ * 发起 `DELETE` 请求。
163
+ *
164
+ * @typeParam T - 期望的响应体类型。
165
+ * @param path - 相对 {@link baseUrl} 的路径。
166
+ * @returns 解析后的响应体(强转为 `T`)。
167
+ * @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
168
+ */
124
169
  async delete(path) {
125
170
  return this.request(path, { method: 'DELETE' });
126
171
  }
127
- /** 使用账号密码登录,成功后把返回 Token 写入当前客户端实例。 */
172
+ /**
173
+ * 使用账号密码登录禅道。
174
+ *
175
+ * 成功后会把返回的 Token 写入当前客户端实例(后续请求自动带上 `Token` 头);
176
+ * 当全局 `persistProfiles` 为真时,会同时把账号、Token、用户信息、服务端配置和
177
+ * 客户端偏好(仅在显式设置过 `timeout` / `insecure` 时)持久化为本地 profile,
178
+ * 并切换为当前 profile,方便下次通过 {@link ZentaoClient.fromProfile} 直接登录态恢复。
179
+ *
180
+ * @param account - 禅道用户账号。
181
+ * @param password - 禅道用户密码(明文,仅在传输层 TLS 内使用)。
182
+ * @returns 登录成功后返回的 API Token。
183
+ * @throws {ZentaoError} `E_LOGIN_FAILED` —— 服务端返回 `status !== "success"` 或缺失 token;
184
+ * 也可能因底层 {@link ZentaoClient.request} 而抛出 HTTP/网络/超时错误。
185
+ */
128
186
  async login(account, password) {
129
187
  const response = await this.post('/users/login', { account, password });
130
188
  if (response.status !== 'success' || !response.token) {
131
189
  throw new ZentaoError('E_LOGIN_FAILED');
132
190
  }
133
191
  this.token = response.token;
192
+ const globals = getGlobalOptions();
193
+ if (globals.persistProfiles) {
194
+ const config = {};
195
+ const timeout = this.timeout ?? globals.timeout;
196
+ const insecure = this.insecure ?? globals.insecure;
197
+ if (timeout !== undefined)
198
+ config.timeout = timeout;
199
+ if (insecure !== undefined)
200
+ config.insecure = insecure;
201
+ await addProfile({
202
+ server: this.siteUrl,
203
+ account,
204
+ token: response.token,
205
+ user: isRecord(response.user) ? response.user : undefined,
206
+ serverConfig: isRecord(response.serverConfig) ? response.serverConfig : undefined,
207
+ config: Object.keys(config).length > 0 ? config : undefined,
208
+ });
209
+ }
134
210
  return response.token;
135
211
  }
136
- /** 创建客户端实例,语义等同于 `new ZentaoClient(options)`。 */
212
+ /**
213
+ * 创建客户端实例,语义等同于 `new ZentaoClient(options)`,便于链式调用。
214
+ *
215
+ * @param options - 客户端配置,参见 {@link ZentaoClientOptions}。
216
+ * @returns 新建的客户端实例。
217
+ * @throws {ZentaoError} 同 {@link ZentaoClient | 构造函数}:`E_INVALID_BASE_URL` 等。
218
+ */
137
219
  static create(options) {
138
220
  return new ZentaoClient(options);
139
221
  }
140
- /** 创建客户端并写入全局选项,供高阶 `request()` 默认使用。 */
222
+ /**
223
+ * 创建客户端并写入全局选项,作为 {@link request} 默认使用的实例。
224
+ *
225
+ * 适合应用入口处一次性完成初始化,后续 `request("module/action", params)` 可省略 `options.client`。
226
+ * 多次调用会覆盖上一次的全局客户端。
227
+ *
228
+ * @param options - 客户端配置,参见 {@link ZentaoClientOptions}。
229
+ * @returns 新建并已注册为全局默认的客户端实例。
230
+ * @throws {ZentaoError} 同 {@link ZentaoClient | 构造函数}:`E_INVALID_BASE_URL` 等。
231
+ */
141
232
  static init(options) {
142
233
  const client = new ZentaoClient(options);
143
234
  setGlobalOptions({ client });
144
235
  return client;
145
236
  }
146
- }
147
- export function createClient(options) {
148
- return ZentaoClient.create(options);
237
+ /**
238
+ * 根据本地持久化 profile 创建客户端。
239
+ *
240
+ * 实际会调用 {@link switchProfile}:若 `profileKey` 存在则刷新其 `lastUsedTime` 并设为当前 profile;
241
+ * 不传 `profileKey` 时使用当前 profile。Profile 中保存的 `timeout` / `insecure` 偏好也会被带回到客户端实例。
242
+ *
243
+ * @param profileKey - 可选的 profile key,格式为 `account@server`;不传时使用当前 profile。
244
+ * @returns 用 profile 还原后的客户端实例。
245
+ * @throws {ZentaoError} `E_NO_PROFILE`(无任何 profile 且未传 key)、`E_PROFILE_NOT_FOUND`(指定 key 不存在)、
246
+ * `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问持久化存储)。
247
+ */
248
+ static async fromProfile(profileKey) {
249
+ // switchProfile 会在内部读取存储、校验 key 并刷新 lastUsedTime 后写回,
250
+ // 若 key 不存在会抛出 E_PROFILE_NOT_FOUND;不传 key 时由 switchCurrentProfile 处理。
251
+ const activeProfile = await switchProfile(profileKey);
252
+ return new ZentaoClient({
253
+ baseUrl: activeProfile.server,
254
+ token: activeProfile.token,
255
+ timeout: typeof activeProfile.config?.timeout === 'number' ? activeProfile.config.timeout : undefined,
256
+ insecure: typeof activeProfile.config?.insecure === 'boolean' ? activeProfile.config.insecure : undefined,
257
+ });
258
+ }
149
259
  }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  export { ZentaoClient } from './client/index.js';
2
2
  export { ERRORS, ZentaoError, type ErrorCode } from './misc/errors.js';
3
3
  export { getGlobalOptions, setGlobalOptions } from './misc/global-options.js';
4
+ export { ZENTAO_PROFILES_STORAGE_KEY, addProfile, deleteProfile, getAllProfiles, getProfile, getProfileKey, switchProfile, } from './profiles/index.js';
4
5
  export { defineModuleActions, defineModules, type DefineModulesOptions, getModule, getModuleAction, } from './modules/registry.js';
5
6
  export { request } from './request/index.js';
6
7
  export { BUILD, VERSION } from './version.js';
7
- export type { ApiListResponse, ApiResponse, ClientRequestOptions, GlobalOptions, HttpMethod, ListPagerInfo, LoginResponse, ModuleAction, ModuleActionMethod, ModuleActionName, ModuleActionPagerGetterMap, ModuleActionParam, ModuleActionParamOption, ModuleActionRequestBody, ModuleActionResponse, ModuleActionResultRender, ModuleActionResultRenderType, ModuleActionResultType, ModuleActionType, ModuleDefinition, ModuleName, Pager, RequestOptions, ResolvedModuleCommand, ResponseData, ServerConfig, ZentaoClientOptions, } from './types/index.js';
8
+ export type { ApiListResponse, ApiResponse, ClientRequestOptions, GlobalOptions, HttpMethod, ListPagerInfo, LoginResponse, ModuleAction, ModuleActionMethod, ModuleActionName, ModuleActionPagerGetterMap, ModuleActionParam, ModuleActionParamOption, ModuleActionRequestBody, ModuleActionResponse, ModuleActionResultRender, ModuleActionResultRenderType, ModuleActionResultType, ModuleActionType, ModuleDefinition, ModuleName, Pager, RequestOptions, ResolvedModuleCommand, ResponseData, ServerConfig, ZentaoProfile, ZentaoProfileConfig, ZentaoProfileRecord, ZentaoProfilesStore, ZentaoClientOptions, } from './types/index.js';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  export { ZentaoClient } from './client/index.js';
2
2
  export { ERRORS, ZentaoError } from './misc/errors.js';
3
3
  export { getGlobalOptions, setGlobalOptions } from './misc/global-options.js';
4
+ export { ZENTAO_PROFILES_STORAGE_KEY, addProfile, deleteProfile, getAllProfiles, getProfile, getProfileKey, switchProfile, } from './profiles/index.js';
4
5
  export { defineModuleActions, defineModules, getModule, getModuleAction, } from './modules/registry.js';
5
6
  export { request } from './request/index.js';
6
7
  export { BUILD, VERSION } from './version.js';
@@ -2,5 +2,7 @@
2
2
  export declare function isNodeRuntime(): boolean;
3
3
  /** 浏览器无法跳过 TLS 校验,因此在发起请求前提前失败。 */
4
4
  export declare function assertInsecureSupported(enabled: boolean | undefined): void;
5
- /** Node.js 中临时关闭 TLS 校验,并在本次请求结束后恢复原值。 */
5
+ /** 发起 fetch 请求;Node.js 下的 `insecure` 只作用于当前 HTTPS 请求。 */
6
+ export declare function fetchWithInsecureTls(enabled: boolean | undefined, url: string, init: RequestInit): Promise<Response>;
7
+ /** 保留给内部测试和兼容调用:校验 TLS 选项,但不再改写进程级环境变量。 */
6
8
  export declare function withInsecureTls<T>(enabled: boolean | undefined, fn: () => Promise<T>): Promise<T>;
@@ -3,28 +3,124 @@ import { ZentaoError } from './errors.js';
3
3
  export function isNodeRuntime() {
4
4
  return typeof process !== 'undefined' && Boolean(process.versions?.node);
5
5
  }
6
+ // 通过函数参数间接化 `import(specifier)`,让打包器无法在静态分析阶段把
7
+ // `node:*` 拉进浏览器 bundle;同时不依赖 `new Function`/`eval`,
8
+ // 在严格 CSP 下也能正常加载。
9
+ function importNodeModule(specifier) {
10
+ return import(specifier);
11
+ }
12
+ function toNodeRequestHeaders(headers) {
13
+ const result = {};
14
+ new Headers(headers).forEach((value, key) => {
15
+ result[key] = value;
16
+ });
17
+ return result;
18
+ }
19
+ function toResponseHeaders(headers) {
20
+ const result = new Headers();
21
+ for (const [key, value] of Object.entries(headers)) {
22
+ if (value === undefined)
23
+ continue;
24
+ result.set(key, Array.isArray(value) ? value.join(', ') : String(value));
25
+ }
26
+ return result;
27
+ }
28
+ async function toNodeBody(body) {
29
+ if (body === undefined || body === null)
30
+ return undefined;
31
+ if (typeof body === 'string')
32
+ return body;
33
+ if (body instanceof Uint8Array)
34
+ return body;
35
+ if (body instanceof ArrayBuffer)
36
+ return new Uint8Array(body);
37
+ if (ArrayBuffer.isView(body)) {
38
+ return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
39
+ }
40
+ if (body instanceof Blob) {
41
+ return new Uint8Array(await body.arrayBuffer());
42
+ }
43
+ return String(body);
44
+ }
45
+ function abortError() {
46
+ return new DOMException('The operation was aborted.', 'AbortError');
47
+ }
48
+ function concatenateChunks(chunks) {
49
+ const totalLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
50
+ const result = new Uint8Array(totalLength);
51
+ let offset = 0;
52
+ for (const chunk of chunks) {
53
+ result.set(chunk, offset);
54
+ offset += chunk.byteLength;
55
+ }
56
+ return result.buffer;
57
+ }
58
+ async function nodeFetchWithTlsOptions(url, init, rejectUnauthorized) {
59
+ const parsed = new URL(url);
60
+ const transport = parsed.protocol === 'https:'
61
+ ? await importNodeModule('node:https')
62
+ : await importNodeModule('node:http');
63
+ const body = await toNodeBody(init.body);
64
+ return new Promise((resolve, reject) => {
65
+ if (init.signal?.aborted) {
66
+ reject(abortError());
67
+ return;
68
+ }
69
+ const request = transport.request(parsed, {
70
+ method: init.method ?? 'GET',
71
+ headers: toNodeRequestHeaders(init.headers),
72
+ rejectUnauthorized,
73
+ }, (response) => {
74
+ const chunks = [];
75
+ response.on('data', (chunk) => {
76
+ chunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk);
77
+ });
78
+ response.on('end', () => {
79
+ cleanup();
80
+ const responseBody = chunks.length > 0 ? concatenateChunks(chunks) : undefined;
81
+ const fetchResponse = new Response(responseBody, {
82
+ status: response.statusCode ?? 200,
83
+ statusText: response.statusMessage ?? '',
84
+ headers: toResponseHeaders(response.headers),
85
+ });
86
+ Object.defineProperty(fetchResponse, 'url', { value: url });
87
+ resolve(fetchResponse);
88
+ });
89
+ });
90
+ const cleanup = () => {
91
+ init.signal?.removeEventListener('abort', abortHandler);
92
+ };
93
+ const abortHandler = () => {
94
+ cleanup();
95
+ request.destroy(abortError());
96
+ };
97
+ request.on('error', (error) => {
98
+ cleanup();
99
+ reject(error);
100
+ });
101
+ init.signal?.addEventListener('abort', abortHandler, { once: true });
102
+ if (body !== undefined)
103
+ request.write(body);
104
+ request.end();
105
+ });
106
+ }
6
107
  /** 浏览器无法跳过 TLS 校验,因此在发起请求前提前失败。 */
7
108
  export function assertInsecureSupported(enabled) {
8
109
  if (enabled && !isNodeRuntime()) {
9
110
  throw new ZentaoError('E_INSECURE_BROWSER');
10
111
  }
11
112
  }
12
- /** Node.js 中临时关闭 TLS 校验,并在本次请求结束后恢复原值。 */
113
+ /** 发起 fetch 请求;Node.js 下的 `insecure` 只作用于当前 HTTPS 请求。 */
114
+ export async function fetchWithInsecureTls(enabled, url, init) {
115
+ if (!enabled)
116
+ return fetch(url, init);
117
+ assertInsecureSupported(enabled);
118
+ return nodeFetchWithTlsOptions(url, init, false);
119
+ }
120
+ /** 保留给内部测试和兼容调用:校验 TLS 选项,但不再改写进程级环境变量。 */
13
121
  export async function withInsecureTls(enabled, fn) {
14
122
  if (!enabled)
15
123
  return fn();
16
124
  assertInsecureSupported(enabled);
17
- const previous = process.env.NODE_TLS_REJECT_UNAUTHORIZED;
18
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
19
- try {
20
- return await fn();
21
- }
22
- finally {
23
- if (previous === undefined) {
24
- delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
25
- }
26
- else {
27
- process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;
28
- }
29
- }
125
+ return fn();
30
126
  }
@@ -1,3 +1,9 @@
1
+ /**
2
+ * SDK 已知错误码到默认消息的映射表。
3
+ *
4
+ * 每条消息允许带 `{key}` 占位符,由 {@link ZentaoError} 构造时使用 `replacements`
5
+ * 进行字面量替换。此对象使用 `as const`,可直接作为类型来源约束错误码。
6
+ */
1
7
  export declare const ERRORS: {
2
8
  readonly E_INVALID_BASE_URL: "Invalid ZenTao baseUrl.";
3
9
  readonly E_NO_GLOBAL_CLIENT: "No global client configured. Call ZentaoClient.init() or pass options.client.";
@@ -6,20 +12,40 @@ export declare const ERRORS: {
6
12
  readonly E_TIMEOUT: "Request timed out.";
7
13
  readonly E_INSECURE_BROWSER: "The insecure option is only supported in Node.js runtimes.";
8
14
  readonly E_LOGIN_FAILED: "ZenTao login failed.";
15
+ readonly E_INVALID_PROFILE: "Invalid ZenTao profile.";
16
+ readonly E_NO_PROFILE: "No ZenTao profile is configured.";
17
+ readonly E_PROFILE_NOT_FOUND: "ZenTao profile not found: {profileKey}";
18
+ readonly E_PROFILE_STORAGE_INVALID: "ZenTao profile storage is not valid JSON.";
19
+ readonly E_PROFILE_STORAGE_UNAVAILABLE: "ZenTao profile storage is unavailable in this runtime.";
9
20
  readonly E_INVALID_MODULE: "Unknown module: {module}";
10
21
  readonly E_INVALID_ACTION: "Unknown action: {module}-{action}";
11
22
  readonly E_INVALID_MODULE_DEFINITION: "Invalid module definition.";
12
23
  readonly E_INVALID_ACTION_DEFINITION: "Invalid module action definition.";
13
24
  readonly E_MISSING_PARAM: "Missing required parameter: {param}";
25
+ readonly E_INVALID_PARAM: "Invalid value for parameter {param}: {value}";
14
26
  readonly E_INVALID_REQUEST_NAME: "Request name must use the form \"moduleName/methodName\".";
27
+ readonly E_API_FAILED: "ZenTao API returned failure: {message}";
15
28
  };
29
+ /** SDK 已知错误码,对应 {@link ERRORS} 的 key。 */
16
30
  export type ErrorCode = keyof typeof ERRORS;
17
- /** SDK 统一错误类型,所有可预期错误都会携带稳定错误码。 */
31
+ /**
32
+ * SDK 统一错误类型。
33
+ *
34
+ * 所有可预期错误(参数缺失、HTTP/网络/超时、登录失败、profile 异常等)都通过
35
+ * `ZentaoError` 抛出并携带稳定 {@link ErrorCode},方便调用方按 `code` 区分处理。
36
+ * 错误消息默认来自 {@link ERRORS} 中的模板,并支持占位符替换。
37
+ */
18
38
  export declare class ZentaoError extends Error {
19
- /** 错误码,对应 {@link ERRORS} 的 key */
39
+ /** 错误码,对应 {@link ERRORS} 的 key;用于稳定地区分错误类型。 */
20
40
  readonly code: ErrorCode;
21
- /** 附加上下文,例如 HTTP 响应详情或原始异常。 */
41
+ /** 附加上下文,例如 HTTP 响应详情、原始异常或失败的禅道响应原文。 */
22
42
  readonly details?: unknown;
23
- /** 根据错误码和占位符替换值创建错误。 */
43
+ /**
44
+ * 构造 SDK 错误实例。
45
+ *
46
+ * @param code - 错误码,必须是 {@link ERRORS} 中已声明的 key。
47
+ * @param replacements - 可选的占位符替换值;遍历后会把 `{key}` 替换为字符串化的值。
48
+ * @param details - 可选的附加上下文(HTTP 响应详情、原始异常等),保存到 {@link details}。
49
+ */
24
50
  constructor(code: ErrorCode, replacements?: Record<string, string | number>, details?: unknown);
25
51
  }