zentao-api 0.2.0-beta.2 → 0.2.1

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.
@@ -4,13 +4,25 @@ import { getModule } from '../modules/registry.js';
4
4
  import { extractPager, extractResult, resolveModuleCommand } from '../modules/resolve.js';
5
5
  /** 将 `moduleName/methodName` 形式的请求名拆成模块名和动作名。 */
6
6
  function splitRequestName(name) {
7
- const index = name.indexOf('/');
8
- if (index <= 0 || index === name.length - 1) {
9
- throw new ZentaoError('E_INVALID_REQUEST_NAME');
7
+ const [moduleName, actionName] = name.split('/');
8
+ // 如果没有指定 actionName
9
+ if (!actionName?.length) {
10
+ return {
11
+ moduleName,
12
+ actionName: 'list',
13
+ };
14
+ }
15
+ // 如果 actionName 为数值
16
+ if (Number.isInteger(Number(actionName))) {
17
+ return {
18
+ moduleName,
19
+ actionName: 'get',
20
+ id: Number(actionName),
21
+ };
10
22
  }
11
23
  return {
12
- moduleName: name.slice(0, index),
13
- actionName: name.slice(index + 1),
24
+ moduleName,
25
+ actionName,
14
26
  };
15
27
  }
16
28
  /** 将禅道原始响应归一化为稳定的 ResponseData 结构。 */
@@ -29,7 +41,7 @@ function normalizeResponse(command, raw, limit) {
29
41
  return {
30
42
  status,
31
43
  message: typeof record.message === 'string' ? record.message : undefined,
32
- data,
44
+ data: data,
33
45
  pager: pager ? {
34
46
  total: Number(pager.recTotal),
35
47
  page: Number(pager.pageID),
@@ -41,6 +53,15 @@ function normalizeResponse(command, raw, limit) {
41
53
  * 按模块动作名请求禅道 API。
42
54
  *
43
55
  * 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
56
+ * 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
57
+ * 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
58
+ *
59
+ * @typeParam T 期望的 `data` 字段类型;不传时为 `unknown`,调用方需要自行收窄。
60
+ * @param name - 模块动作名,例如 `product/list`。
61
+ * @param params - 请求参数。
62
+ * @param options - 请求选项。
63
+ * @returns 归一化后的禅道 API 响应。
64
+ * @throws {ZentaoError} 传输层错误、参数缺失或 `throwOnFail` 启用时的业务失败。
44
65
  */
45
66
  export async function request(name, params = {}, options = {}) {
46
67
  const globals = getGlobalOptions();
@@ -48,11 +69,15 @@ export async function request(name, params = {}, options = {}) {
48
69
  if (!client) {
49
70
  throw new ZentaoError('E_NO_GLOBAL_CLIENT');
50
71
  }
51
- const { moduleName, actionName } = splitRequestName(name);
72
+ const { moduleName, actionName, id } = splitRequestName(name);
52
73
  const module = getModule(moduleName);
53
74
  // recPerPage 是最常用的列表参数,允许在全局或本次调用中统一覆盖。
54
75
  const recPerPage = params.recPerPage ?? options.recPerPage ?? globals.recPerPage;
55
- const mergedParams = recPerPage === undefined ? params : { ...params, recPerPage };
76
+ const mergedParams = {
77
+ ...(id !== undefined ? { id } : {}),
78
+ ...params,
79
+ ...(recPerPage !== undefined ? { recPerPage } : {}),
80
+ };
56
81
  const command = resolveModuleCommand(module, actionName, mergedParams);
57
82
  const raw = await client.request(command.path, {
58
83
  method: String(command.action.method).toUpperCase(),
@@ -61,5 +86,9 @@ export async function request(name, params = {}, options = {}) {
61
86
  timeout: options.timeout ?? globals.timeout,
62
87
  insecure: options.insecure ?? globals.insecure,
63
88
  });
64
- return normalizeResponse(command, raw, options.limit ?? globals.limit);
89
+ const response = normalizeResponse(command, raw, options.limit ?? globals.limit);
90
+ if (response.status === 'fail' && (options.throwOnFail ?? globals.throwOnFail)) {
91
+ throw new ZentaoError('E_API_FAILED', { message: response.message ?? '' }, response);
92
+ }
93
+ return response;
65
94
  }
@@ -24,6 +24,8 @@ export interface GlobalOptions {
24
24
  insecure?: boolean;
25
25
  /** 是否在登录成功后把账号、Token 和配置持久化为本地 profile。 */
26
26
  persistProfiles?: boolean;
27
+ /** 当禅道服务端返回 `{ status: "fail" }` 时是否抛出 `E_API_FAILED`,默认 false。 */
28
+ throwOnFail?: boolean;
27
29
  }
28
30
  /** SDK 支持的 HTTP 方法。 */
29
31
  export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
@@ -32,7 +34,7 @@ export interface ClientRequestOptions {
32
34
  /** HTTP 方法,默认 `GET`。 */
33
35
  method?: HttpMethod;
34
36
  /** JSON 请求体;`GET` 请求会忽略该字段。 */
35
- body?: Record<string, unknown>;
37
+ body?: unknown;
36
38
  /** URL 查询参数;`undefined` 值会被跳过。 */
37
39
  query?: Record<string, string | number | boolean | undefined>;
38
40
  /** 单次请求超时时间,优先级高于全局和客户端默认值。 */
@@ -52,15 +54,20 @@ export interface RequestOptions {
52
54
  timeout?: number;
53
55
  /** 本次调用 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
54
56
  insecure?: boolean;
57
+ /**
58
+ * 当禅道服务端返回 `{ status: "fail" }` 时是否抛出 `E_API_FAILED`。
59
+ * 不传时回落到全局 `throwOnFail`,默认 false(保留原始失败响应)。
60
+ */
61
+ throwOnFail?: boolean;
55
62
  }
56
63
  /** 高阶 `request()` 归一化后的返回数据。 */
57
- export interface ResponseData {
64
+ export interface ResponseData<T = unknown> {
58
65
  /** 禅道服务端状态;非标准响应会按成功响应包装到 `data`。 */
59
66
  status: 'success' | 'fail';
60
67
  /** 禅道服务端返回的消息。 */
61
68
  message?: string;
62
69
  /** 根据模块动作 `resultGetter` 提取后的业务数据。 */
63
- data?: any;
70
+ data?: T;
64
71
  /** 统一分页信息。 */
65
72
  pager?: {
66
73
  /** 总记录数。 */
@@ -127,8 +134,6 @@ export interface ZentaoProfileConfig {
127
134
  defaultRecPerPage?: number;
128
135
  /** 是否跳过 TLS 证书验证;仅 Node.js 运行时支持。 */
129
136
  insecure?: boolean;
130
- /** 是否将对象属性中的 HTML 转换为 Markdown。 */
131
- htmlToMarkdown?: boolean;
132
137
  /** 请求超时时间,单位毫秒。 */
133
138
  timeout?: number;
134
139
  /** 是否在批量操作出错时停止执行后续操作。 */
@@ -8,7 +8,18 @@ export function normalizeSiteUrl(baseUrl) {
8
8
  const trimmed = baseUrl.trim().replace(/\/+$/, '');
9
9
  if (!trimmed)
10
10
  throw new ZentaoError('E_INVALID_BASE_URL');
11
- return trimmed.replace(/\/api\.php\/v2$/i, '');
11
+ const siteUrl = trimmed.replace(/\/api\.php\/v2$/i, '');
12
+ let parsed;
13
+ try {
14
+ parsed = new URL(siteUrl);
15
+ }
16
+ catch {
17
+ throw new ZentaoError('E_INVALID_BASE_URL');
18
+ }
19
+ if (!['http:', 'https:'].includes(parsed.protocol) || parsed.search || parsed.hash) {
20
+ throw new ZentaoError('E_INVALID_BASE_URL');
21
+ }
22
+ return parsed.toString().replace(/\/+$/, '');
12
23
  }
13
24
  export function getNestedValue(obj, path) {
14
25
  const keys = path.split('.');
package/dist/version.d.ts CHANGED
@@ -1,2 +1,12 @@
1
+ /**
2
+ * 构建标识,由构建脚本通过 `__ZENTAO_API_BUILD__` 注入。
3
+ *
4
+ * 通常是 commit hash 或 CI 构建号;本地 `tsc` 直接编译时回落为 `'development'`。
5
+ */
1
6
  export declare const BUILD: string;
7
+ /**
8
+ * SDK 版本号,由构建脚本通过 `__ZENTAO_API_VERSION__` 注入。
9
+ *
10
+ * 通常等于 `package.json` 的 `version` 字段;本地 `tsc` 直接编译时回落为 `'0.0.0-dev'`。
11
+ */
2
12
  export declare const VERSION: string;
package/dist/version.js CHANGED
@@ -1,4 +1,14 @@
1
- const fallbackBuild = "2026-05-11T13:46:14.715Z";
2
- const fallbackVersion = "0.2.0-beta.2";
1
+ const fallbackBuild = "2026-06-23T08:50:58.737Z";
2
+ const fallbackVersion = "0.2.1";
3
+ /**
4
+ * 构建标识,由构建脚本通过 `__ZENTAO_API_BUILD__` 注入。
5
+ *
6
+ * 通常是 commit hash 或 CI 构建号;本地 `tsc` 直接编译时回落为 `'development'`。
7
+ */
3
8
  export const BUILD = typeof __ZENTAO_API_BUILD__ === 'string' ? __ZENTAO_API_BUILD__ : fallbackBuild;
9
+ /**
10
+ * SDK 版本号,由构建脚本通过 `__ZENTAO_API_VERSION__` 注入。
11
+ *
12
+ * 通常等于 `package.json` 的 `version` 字段;本地 `tsc` 直接编译时回落为 `'0.0.0-dev'`。
13
+ */
4
14
  export const VERSION = typeof __ZENTAO_API_VERSION__ === 'string' ? __ZENTAO_API_VERSION__ : fallbackVersion;
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "zentao-api",
3
- "version": "0.2.0-beta.2",
3
+ "version": "0.2.1",
4
4
  "type": "module",
5
5
  "description": "Browser and Node.js SDK for ZenTao API",
6
6
  "license": "MIT",
7
7
  "author": "Sun Hao <sunhao@chandao.com>",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/catouse/zentao-api"
10
+ "url": "https://github.com/easysoft/zentao-api"
11
11
  },
12
12
  "keywords": [
13
13
  "zentao",
@@ -18,12 +18,21 @@
18
18
  ],
19
19
  "main": "./dist/index.js",
20
20
  "types": "./dist/index.d.ts",
21
+ "sideEffects": [
22
+ "./dist/browser-global.js",
23
+ "./dist/browser/zentao-api.global.js"
24
+ ],
21
25
  "exports": {
22
26
  ".": {
23
27
  "types": "./dist/index.d.ts",
28
+ "browser": "./dist/browser.js",
24
29
  "import": "./dist/index.js"
25
30
  },
26
- "./browser": "./dist/browser/zentao-api.global.js",
31
+ "./browser": {
32
+ "types": "./dist/browser.d.ts",
33
+ "import": "./dist/browser.js"
34
+ },
35
+ "./browser/global": "./dist/browser/zentao-api.global.js",
27
36
  "./package.json": "./package.json"
28
37
  },
29
38
  "files": [
@@ -50,13 +59,9 @@
50
59
  "check": "bun run test:coverage && bun run typecheck && bun run typecheck:tests && bun run registry:check && bun run build && bun run smoke:node && bun run smoke:package",
51
60
  "prepublishOnly": "bun run check"
52
61
  },
53
- "dependencies": {
54
- "turndown": "^7.2.0"
55
- },
56
62
  "devDependencies": {
57
63
  "@types/bun": "^1.3.13",
58
64
  "@types/node": "^24.10.0",
59
- "@types/turndown": "^5.0.5",
60
65
  "typedoc": "^0.28.19",
61
66
  "typedoc-plugin-markdown": "^4.11.0",
62
67
  "typedoc-vitepress-theme": "^1.1.2",
@@ -1 +0,0 @@
1
- export * from '../index.js';
@@ -1,8 +0,0 @@
1
- import * as api from '../index.js';
2
- // Bun 的 IIFE bundle 不总是自动挂载 globalName;这里显式暴露浏览器全局对象。
3
- const globalTarget = globalThis;
4
- globalTarget.ZentaoAPI = api;
5
- if (globalTarget.window) {
6
- globalTarget.window.ZentaoAPI = api;
7
- }
8
- export * from '../index.js';