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.
- package/README.md +161 -37
- package/dist/browser/zentao-api.global.js +2 -1
- package/dist/browser-global.d.ts +1 -0
- package/dist/browser-global.js +12 -0
- package/dist/browser.d.ts +1 -1
- package/dist/browser.js +4 -1
- package/dist/client/index.d.ts +119 -16
- package/dist/client/index.js +151 -41
- package/dist/index.d.ts +2 -1
- package/dist/index.js +1 -0
- package/dist/misc/environment.d.ts +3 -1
- package/dist/misc/environment.js +110 -14
- package/dist/misc/errors.d.ts +30 -4
- package/dist/misc/errors.js +29 -4
- package/dist/misc/global-options.d.ts +15 -2
- package/dist/misc/global-options.js +15 -2
- package/dist/modules/registry.d.ts +71 -8
- package/dist/modules/registry.js +123 -31
- package/dist/modules/resolve.js +29 -16
- package/dist/profiles/index.d.ts +76 -0
- package/dist/profiles/index.js +294 -0
- package/dist/request/index.d.ts +10 -1
- package/dist/request/index.js +15 -2
- package/dist/types/index.d.ts +69 -3
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +23 -0
- package/dist/version.d.ts +10 -0
- package/dist/version.js +12 -2
- package/package.json +24 -9
- package/dist/misc/browser-global.d.ts +0 -1
- package/dist/misc/browser-global.js +0 -8
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
import { isNodeRuntime } from '../misc/environment.js';
|
|
3
|
+
import { normalizeSiteUrl } from '../utils/index.js';
|
|
4
|
+
/**
|
|
5
|
+
* 浏览器环境下用于在 `localStorage` 中保存 profile 数据的 key。
|
|
6
|
+
*
|
|
7
|
+
* Node.js 环境会改用文件 `~/.config/zentao/zentao.json`,与此常量无关。
|
|
8
|
+
*/
|
|
9
|
+
export const ZENTAO_PROFILES_STORAGE_KEY = 'ZENTAO_PROFILES';
|
|
10
|
+
const PROFILE_FILE_PARTS = ['.config', 'zentao', 'zentao.json'];
|
|
11
|
+
function isRecord(value) {
|
|
12
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
13
|
+
}
|
|
14
|
+
function cloneJson(value) {
|
|
15
|
+
if (value === undefined)
|
|
16
|
+
return value;
|
|
17
|
+
return JSON.parse(JSON.stringify(value));
|
|
18
|
+
}
|
|
19
|
+
function nowString() {
|
|
20
|
+
return new Date().toISOString();
|
|
21
|
+
}
|
|
22
|
+
// 通过函数参数间接化 `import(specifier)`,避免打包器把 Node 内置模块拉进
|
|
23
|
+
// 浏览器 bundle;同时不依赖 `new Function`/`eval`,对严格 CSP 友好。
|
|
24
|
+
function importNodeModule(specifier) {
|
|
25
|
+
return import(specifier);
|
|
26
|
+
}
|
|
27
|
+
// 进程内串行锁:所有 read-modify-write 类的 profile 操作都通过这个队列,
|
|
28
|
+
// 避免并发 `addProfile`/`switchProfile` 出现 lost update(写文件本身是原子
|
|
29
|
+
// rename,但 read→modify→write 之间没有跨步保护)。跨进程并发不在保证范围内。
|
|
30
|
+
let storeMutex = Promise.resolve();
|
|
31
|
+
function withStoreMutex(operation) {
|
|
32
|
+
const next = storeMutex.then(operation, operation);
|
|
33
|
+
storeMutex = next.catch(() => undefined);
|
|
34
|
+
return next;
|
|
35
|
+
}
|
|
36
|
+
function getBrowserStorage() {
|
|
37
|
+
try {
|
|
38
|
+
return globalThis.localStorage;
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async function getProfileFilePath() {
|
|
45
|
+
const path = await importNodeModule('node:path');
|
|
46
|
+
const home = process.env.HOME
|
|
47
|
+
?? process.env.USERPROFILE
|
|
48
|
+
?? (await importNodeModule('node:os')).homedir();
|
|
49
|
+
if (!home) {
|
|
50
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
51
|
+
}
|
|
52
|
+
return path.join(home, ...PROFILE_FILE_PARTS);
|
|
53
|
+
}
|
|
54
|
+
function profileKeyFromParts(account, server) {
|
|
55
|
+
const normalizedAccount = account.trim();
|
|
56
|
+
const normalizedServer = normalizeSiteUrl(server);
|
|
57
|
+
if (!normalizedAccount)
|
|
58
|
+
throw new ZentaoError('E_INVALID_PROFILE');
|
|
59
|
+
return `${normalizedAccount}@${normalizedServer}`;
|
|
60
|
+
}
|
|
61
|
+
function normalizeProfile(profile) {
|
|
62
|
+
if (!isRecord(profile) || typeof profile.server !== 'string' || typeof profile.account !== 'string' || typeof profile.token !== 'string') {
|
|
63
|
+
throw new ZentaoError('E_INVALID_PROFILE');
|
|
64
|
+
}
|
|
65
|
+
const token = profile.token.trim();
|
|
66
|
+
if (!token)
|
|
67
|
+
throw new ZentaoError('E_INVALID_PROFILE');
|
|
68
|
+
const copy = cloneJson(profile);
|
|
69
|
+
delete copy.key;
|
|
70
|
+
return {
|
|
71
|
+
...copy,
|
|
72
|
+
server: normalizeSiteUrl(profile.server),
|
|
73
|
+
account: profile.account.trim(),
|
|
74
|
+
token,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function normalizeStore(raw) {
|
|
78
|
+
if (!isRecord(raw))
|
|
79
|
+
return { profiles: [] };
|
|
80
|
+
const profiles = Array.isArray(raw.profiles)
|
|
81
|
+
? raw.profiles.flatMap((profile) => {
|
|
82
|
+
try {
|
|
83
|
+
return [normalizeProfile(profile)];
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
: [];
|
|
90
|
+
const currentProfile = typeof raw.currentProfile === 'string' ? raw.currentProfile : undefined;
|
|
91
|
+
return currentProfile ? { currentProfile, profiles } : { profiles };
|
|
92
|
+
}
|
|
93
|
+
function parseStore(text) {
|
|
94
|
+
try {
|
|
95
|
+
return normalizeStore(JSON.parse(text));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
throw new ZentaoError('E_PROFILE_STORAGE_INVALID', undefined, error);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function readStore() {
|
|
102
|
+
if (isNodeRuntime()) {
|
|
103
|
+
const fs = await importNodeModule('node:fs/promises');
|
|
104
|
+
const file = await getProfileFilePath();
|
|
105
|
+
try {
|
|
106
|
+
return parseStore(await fs.readFile(file, 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
catch (error) {
|
|
109
|
+
if (error.code === 'ENOENT') {
|
|
110
|
+
return { profiles: [] };
|
|
111
|
+
}
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const storage = getBrowserStorage();
|
|
116
|
+
if (!storage) {
|
|
117
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
118
|
+
}
|
|
119
|
+
const text = storage.getItem(ZENTAO_PROFILES_STORAGE_KEY);
|
|
120
|
+
return text ? parseStore(text) : { profiles: [] };
|
|
121
|
+
}
|
|
122
|
+
async function writeStore(store) {
|
|
123
|
+
const normalizedStore = normalizeStore(store);
|
|
124
|
+
const text = `${JSON.stringify(normalizedStore, null, 2)}\n`;
|
|
125
|
+
if (isNodeRuntime()) {
|
|
126
|
+
const fs = await importNodeModule('node:fs/promises');
|
|
127
|
+
const path = await importNodeModule('node:path');
|
|
128
|
+
const file = await getProfileFilePath();
|
|
129
|
+
const dir = path.dirname(file);
|
|
130
|
+
const tempFile = path.join(dir, `.zentao.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
131
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
132
|
+
await fs.chmod(dir, 0o700).catch(() => undefined);
|
|
133
|
+
try {
|
|
134
|
+
await fs.writeFile(tempFile, text, { encoding: 'utf8', mode: 0o600 });
|
|
135
|
+
await fs.rename(tempFile, file);
|
|
136
|
+
await fs.chmod(file, 0o600).catch(() => undefined);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
await fs.rm(tempFile, { force: true }).catch(() => undefined);
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const storage = getBrowserStorage();
|
|
145
|
+
if (!storage) {
|
|
146
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
147
|
+
}
|
|
148
|
+
storage.setItem(ZENTAO_PROFILES_STORAGE_KEY, text);
|
|
149
|
+
}
|
|
150
|
+
function toRecord(profile) {
|
|
151
|
+
const normalized = normalizeProfile(profile);
|
|
152
|
+
return {
|
|
153
|
+
...cloneJson(normalized),
|
|
154
|
+
key: getProfileKey(normalized),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function findProfile(store, profileKey) {
|
|
158
|
+
return store.profiles.find((profile) => getProfileKey(profile) === profileKey);
|
|
159
|
+
}
|
|
160
|
+
function setFallbackCurrentProfile(store) {
|
|
161
|
+
if (!store.currentProfile || !findProfile(store, store.currentProfile)) {
|
|
162
|
+
const fallback = store.profiles.at(-1);
|
|
163
|
+
store.currentProfile = fallback ? getProfileKey(fallback) : undefined;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* 根据 profile 的账号和禅道站点地址生成稳定 key。
|
|
168
|
+
*
|
|
169
|
+
* Key 格式为 `account@server`,其中 `server` 会经过 {@link normalizeSiteUrl} 规范化,
|
|
170
|
+
* 因此即使传入末尾带 `/` 或 `/api.php/v2` 的地址,也会得到一致的结果。
|
|
171
|
+
*
|
|
172
|
+
* @param profile - 只需要包含 `account` 和 `server` 两个字段。
|
|
173
|
+
* @returns 形如 `admin@https://zentao.example.com` 的 profile key。
|
|
174
|
+
* @throws {ZentaoError} `E_INVALID_PROFILE`(账号为空白)或 `E_INVALID_BASE_URL`(`server` 不合法)。
|
|
175
|
+
*/
|
|
176
|
+
export function getProfileKey(profile) {
|
|
177
|
+
return profileKeyFromParts(profile.account, profile.server);
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* 列出本地保存的所有 profile。
|
|
181
|
+
*
|
|
182
|
+
* Node.js 下从 `~/.config/zentao/zentao.json` 读取;浏览器下从 `localStorage` 读取。
|
|
183
|
+
* 读取过程不会写回存储;存储中无法解析的条目会被静默忽略,不会影响其余 profile。
|
|
184
|
+
*
|
|
185
|
+
* @returns 当前存储中的所有 profile(带 `key` 字段),文件不存在时返回空数组。
|
|
186
|
+
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID`(存储内容不是合法 JSON)或
|
|
187
|
+
* `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问存储)。
|
|
188
|
+
*/
|
|
189
|
+
export async function getAllProfiles() {
|
|
190
|
+
const store = await readStore();
|
|
191
|
+
return store.profiles.map(toRecord);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* 获取指定 profile。
|
|
195
|
+
*
|
|
196
|
+
* @param profileKey - 可选的 profile key(`account@server`);不传时返回当前(最近一次切换的)profile。
|
|
197
|
+
* @returns 命中的 profile(带 `key` 字段);当 key 不存在或尚未配置当前 profile 时返回 `undefined`。
|
|
198
|
+
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID` / `E_PROFILE_STORAGE_UNAVAILABLE`。
|
|
199
|
+
*/
|
|
200
|
+
export async function getProfile(profileKey) {
|
|
201
|
+
const store = await readStore();
|
|
202
|
+
const key = profileKey ?? store.currentProfile;
|
|
203
|
+
if (!key)
|
|
204
|
+
return undefined;
|
|
205
|
+
const profile = findProfile(store, key);
|
|
206
|
+
return profile ? toRecord(profile) : undefined;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* 添加或覆盖一个本地 profile,并把它设置为当前使用的 profile。
|
|
210
|
+
*
|
|
211
|
+
* 行为细节:
|
|
212
|
+
* - 同 key(`account@server`)已存在时会**整体覆盖**而非合并字段。
|
|
213
|
+
* - 写入时会自动补齐 `loginTime` 与 `lastUsedTime`(若调用方未提供则使用当前 ISO 时间)。
|
|
214
|
+
* - 操作通过进程内串行锁保护 read-modify-write,避免并发调用导致的 lost update;跨进程并发不在保证范围。
|
|
215
|
+
* - 实际写入使用临时文件 + `rename` 的原子方式,并将文件与目录权限收紧到 `0600`/`0700`(Node.js 下)。
|
|
216
|
+
*
|
|
217
|
+
* @param profile - 要写入的 profile,必须至少包含 `server`、`account`、`token`。
|
|
218
|
+
* @returns 实际写入并附带 `key` 字段的 profile 记录。
|
|
219
|
+
* @throws {ZentaoError} `E_INVALID_PROFILE`(必填字段缺失或 token 为空白)、
|
|
220
|
+
* `E_INVALID_BASE_URL`、`E_PROFILE_STORAGE_INVALID`、`E_PROFILE_STORAGE_UNAVAILABLE`。
|
|
221
|
+
*/
|
|
222
|
+
export function addProfile(profile) {
|
|
223
|
+
return withStoreMutex(async () => {
|
|
224
|
+
const store = await readStore();
|
|
225
|
+
const timestamp = nowString();
|
|
226
|
+
const normalized = normalizeProfile({
|
|
227
|
+
...profile,
|
|
228
|
+
loginTime: profile.loginTime ?? timestamp,
|
|
229
|
+
lastUsedTime: profile.lastUsedTime ?? timestamp,
|
|
230
|
+
});
|
|
231
|
+
const profileKey = getProfileKey(normalized);
|
|
232
|
+
const index = store.profiles.findIndex((item) => getProfileKey(item) === profileKey);
|
|
233
|
+
if (index >= 0) {
|
|
234
|
+
store.profiles[index] = normalized;
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
store.profiles.push(normalized);
|
|
238
|
+
}
|
|
239
|
+
store.currentProfile = profileKey;
|
|
240
|
+
await writeStore(store);
|
|
241
|
+
return toRecord(normalized);
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* 删除指定 profile。
|
|
246
|
+
*
|
|
247
|
+
* 若被删除的是当前 profile,会回退为列表中最近一次写入的 profile;若已无任何 profile,
|
|
248
|
+
* 当前 profile 会被清空。操作同样通过进程内串行锁保护。
|
|
249
|
+
*
|
|
250
|
+
* @param profileKey - 要删除的 profile key。
|
|
251
|
+
* @returns 当且仅当确实删除了某条记录时返回 `true`;key 不存在时返回 `false` 且不会写盘。
|
|
252
|
+
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID` / `E_PROFILE_STORAGE_UNAVAILABLE`。
|
|
253
|
+
*/
|
|
254
|
+
export function deleteProfile(profileKey) {
|
|
255
|
+
return withStoreMutex(async () => {
|
|
256
|
+
const store = await readStore();
|
|
257
|
+
const nextProfiles = store.profiles.filter((profile) => getProfileKey(profile) !== profileKey);
|
|
258
|
+
if (nextProfiles.length === store.profiles.length)
|
|
259
|
+
return false;
|
|
260
|
+
store.profiles = nextProfiles;
|
|
261
|
+
setFallbackCurrentProfile(store);
|
|
262
|
+
await writeStore(store);
|
|
263
|
+
return true;
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 切换当前使用的 profile,并刷新其 `lastUsedTime`。
|
|
268
|
+
*
|
|
269
|
+
* 不传 `profileKey` 时使用当前 profile(相当于把当前 profile 的 `lastUsedTime` 刷新一遍)。
|
|
270
|
+
* 切换成功后会立即写回存储,由进程内串行锁保护。
|
|
271
|
+
*
|
|
272
|
+
* @param profileKey - 可选的目标 profile key;不传则使用当前 profile。
|
|
273
|
+
* @returns 切换后生效的 profile 记录(带 `key` 字段)。
|
|
274
|
+
* @throws {ZentaoError} `E_NO_PROFILE`(未配置任何当前 profile 且未传 key)、
|
|
275
|
+
* `E_PROFILE_NOT_FOUND`(目标 key 不存在)、`E_PROFILE_STORAGE_INVALID` /
|
|
276
|
+
* `E_PROFILE_STORAGE_UNAVAILABLE`。
|
|
277
|
+
*/
|
|
278
|
+
export function switchProfile(profileKey) {
|
|
279
|
+
return withStoreMutex(async () => {
|
|
280
|
+
const store = await readStore();
|
|
281
|
+
const key = profileKey ?? store.currentProfile;
|
|
282
|
+
if (!key) {
|
|
283
|
+
throw new ZentaoError('E_NO_PROFILE');
|
|
284
|
+
}
|
|
285
|
+
const profile = findProfile(store, key);
|
|
286
|
+
if (!profile) {
|
|
287
|
+
throw new ZentaoError('E_PROFILE_NOT_FOUND', { profileKey: key });
|
|
288
|
+
}
|
|
289
|
+
profile.lastUsedTime = nowString();
|
|
290
|
+
store.currentProfile = key;
|
|
291
|
+
await writeStore(store);
|
|
292
|
+
return toRecord(profile);
|
|
293
|
+
});
|
|
294
|
+
}
|
package/dist/request/index.d.ts
CHANGED
|
@@ -3,5 +3,14 @@ import type { RequestOptions, ResponseData } from '../types/index.js';
|
|
|
3
3
|
* 按模块动作名请求禅道 API。
|
|
4
4
|
*
|
|
5
5
|
* 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
|
|
6
|
+
* 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
|
|
7
|
+
* 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
|
|
8
|
+
*
|
|
9
|
+
* @typeParam T 期望的 `data` 字段类型;不传时为 `unknown`,调用方需要自行收窄。
|
|
10
|
+
* @param name - 模块动作名,例如 `product/list`。
|
|
11
|
+
* @param params - 请求参数。
|
|
12
|
+
* @param options - 请求选项。
|
|
13
|
+
* @returns 归一化后的禅道 API 响应。
|
|
14
|
+
* @throws {ZentaoError} 传输层错误、参数缺失或 `throwOnFail` 启用时的业务失败。
|
|
6
15
|
*/
|
|
7
|
-
export declare function request(name: `${string}/${string}`, params?: Record<string, unknown>, options?: RequestOptions): Promise<ResponseData
|
|
16
|
+
export declare function request<T = unknown>(name: `${string}/${string}`, params?: Record<string, unknown>, options?: RequestOptions): Promise<ResponseData<T>>;
|
package/dist/request/index.js
CHANGED
|
@@ -29,7 +29,7 @@ function normalizeResponse(command, raw, limit) {
|
|
|
29
29
|
return {
|
|
30
30
|
status,
|
|
31
31
|
message: typeof record.message === 'string' ? record.message : undefined,
|
|
32
|
-
data,
|
|
32
|
+
data: data,
|
|
33
33
|
pager: pager ? {
|
|
34
34
|
total: Number(pager.recTotal),
|
|
35
35
|
page: Number(pager.pageID),
|
|
@@ -41,6 +41,15 @@ function normalizeResponse(command, raw, limit) {
|
|
|
41
41
|
* 按模块动作名请求禅道 API。
|
|
42
42
|
*
|
|
43
43
|
* 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
|
|
44
|
+
* 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
|
|
45
|
+
* 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
|
|
46
|
+
*
|
|
47
|
+
* @typeParam T 期望的 `data` 字段类型;不传时为 `unknown`,调用方需要自行收窄。
|
|
48
|
+
* @param name - 模块动作名,例如 `product/list`。
|
|
49
|
+
* @param params - 请求参数。
|
|
50
|
+
* @param options - 请求选项。
|
|
51
|
+
* @returns 归一化后的禅道 API 响应。
|
|
52
|
+
* @throws {ZentaoError} 传输层错误、参数缺失或 `throwOnFail` 启用时的业务失败。
|
|
44
53
|
*/
|
|
45
54
|
export async function request(name, params = {}, options = {}) {
|
|
46
55
|
const globals = getGlobalOptions();
|
|
@@ -61,5 +70,9 @@ export async function request(name, params = {}, options = {}) {
|
|
|
61
70
|
timeout: options.timeout ?? globals.timeout,
|
|
62
71
|
insecure: options.insecure ?? globals.insecure,
|
|
63
72
|
});
|
|
64
|
-
|
|
73
|
+
const response = normalizeResponse(command, raw, options.limit ?? globals.limit);
|
|
74
|
+
if (response.status === 'fail' && (options.throwOnFail ?? globals.throwOnFail)) {
|
|
75
|
+
throw new ZentaoError('E_API_FAILED', { message: response.message ?? '' }, response);
|
|
76
|
+
}
|
|
77
|
+
return response;
|
|
65
78
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface GlobalOptions {
|
|
|
22
22
|
timeout?: number;
|
|
23
23
|
/** 默认 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
24
24
|
insecure?: boolean;
|
|
25
|
+
/** 是否在登录成功后把账号、Token 和配置持久化为本地 profile。 */
|
|
26
|
+
persistProfiles?: boolean;
|
|
27
|
+
/** 当禅道服务端返回 `{ status: "fail" }` 时是否抛出 `E_API_FAILED`,默认 false。 */
|
|
28
|
+
throwOnFail?: boolean;
|
|
25
29
|
}
|
|
26
30
|
/** SDK 支持的 HTTP 方法。 */
|
|
27
31
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
@@ -30,7 +34,7 @@ export interface ClientRequestOptions {
|
|
|
30
34
|
/** HTTP 方法,默认 `GET`。 */
|
|
31
35
|
method?: HttpMethod;
|
|
32
36
|
/** JSON 请求体;`GET` 请求会忽略该字段。 */
|
|
33
|
-
body?:
|
|
37
|
+
body?: unknown;
|
|
34
38
|
/** URL 查询参数;`undefined` 值会被跳过。 */
|
|
35
39
|
query?: Record<string, string | number | boolean | undefined>;
|
|
36
40
|
/** 单次请求超时时间,优先级高于全局和客户端默认值。 */
|
|
@@ -50,15 +54,20 @@ export interface RequestOptions {
|
|
|
50
54
|
timeout?: number;
|
|
51
55
|
/** 本次调用 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
52
56
|
insecure?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* 当禅道服务端返回 `{ status: "fail" }` 时是否抛出 `E_API_FAILED`。
|
|
59
|
+
* 不传时回落到全局 `throwOnFail`,默认 false(保留原始失败响应)。
|
|
60
|
+
*/
|
|
61
|
+
throwOnFail?: boolean;
|
|
53
62
|
}
|
|
54
63
|
/** 高阶 `request()` 归一化后的返回数据。 */
|
|
55
|
-
export interface ResponseData {
|
|
64
|
+
export interface ResponseData<T = unknown> {
|
|
56
65
|
/** 禅道服务端状态;非标准响应会按成功响应包装到 `data`。 */
|
|
57
66
|
status: 'success' | 'fail';
|
|
58
67
|
/** 禅道服务端返回的消息。 */
|
|
59
68
|
message?: string;
|
|
60
69
|
/** 根据模块动作 `resultGetter` 提取后的业务数据。 */
|
|
61
|
-
data?:
|
|
70
|
+
data?: T;
|
|
62
71
|
/** 统一分页信息。 */
|
|
63
72
|
pager?: {
|
|
64
73
|
/** 总记录数。 */
|
|
@@ -98,6 +107,10 @@ export interface ApiListResponse extends ApiResponse {
|
|
|
98
107
|
export interface LoginResponse extends ApiResponse {
|
|
99
108
|
/** 登录成功后返回的 API Token。 */
|
|
100
109
|
token?: string;
|
|
110
|
+
/** 部分禅道环境会随登录响应返回用户信息。 */
|
|
111
|
+
user?: Record<string, unknown>;
|
|
112
|
+
/** 部分禅道环境会随登录响应返回服务端配置。 */
|
|
113
|
+
serverConfig?: ServerConfig;
|
|
101
114
|
}
|
|
102
115
|
/** 禅道 `?mode=getconfig` 返回的服务端配置。 */
|
|
103
116
|
export interface ServerConfig {
|
|
@@ -111,6 +124,59 @@ export interface ServerConfig {
|
|
|
111
124
|
viewVar: string;
|
|
112
125
|
sessionVar: string;
|
|
113
126
|
}
|
|
127
|
+
/** 保存到本地 profile 中的客户端偏好配置。 */
|
|
128
|
+
export interface ZentaoProfileConfig {
|
|
129
|
+
/** 默认输出格式,供 CLI 等上层应用复用。 */
|
|
130
|
+
defaultOutputFormat?: 'markdown' | 'json' | 'raw';
|
|
131
|
+
/** 界面语言。 */
|
|
132
|
+
lang?: string;
|
|
133
|
+
/** 默认分页大小。 */
|
|
134
|
+
defaultRecPerPage?: number;
|
|
135
|
+
/** 是否跳过 TLS 证书验证;仅 Node.js 运行时支持。 */
|
|
136
|
+
insecure?: boolean;
|
|
137
|
+
/** 请求超时时间,单位毫秒。 */
|
|
138
|
+
timeout?: number;
|
|
139
|
+
/** 是否在批量操作出错时停止执行后续操作。 */
|
|
140
|
+
batchFailFast?: boolean;
|
|
141
|
+
/** JSON 格式化时是否添加缩进。 */
|
|
142
|
+
jsonPretty?: boolean;
|
|
143
|
+
/** 模块级分页偏好。 */
|
|
144
|
+
pagers?: Record<string, number>;
|
|
145
|
+
/** 允许上层应用保存自定义配置。 */
|
|
146
|
+
[key: string]: unknown;
|
|
147
|
+
}
|
|
148
|
+
/** 本地持久化的禅道账号 profile。 */
|
|
149
|
+
export interface ZentaoProfile {
|
|
150
|
+
/** 禅道站点根地址,不包含 `/api.php/v2`。 */
|
|
151
|
+
server: string;
|
|
152
|
+
/** 用户账号。 */
|
|
153
|
+
account: string;
|
|
154
|
+
/** 禅道 API Token。 */
|
|
155
|
+
token: string;
|
|
156
|
+
/** 登录验证通过后得到的用户信息。 */
|
|
157
|
+
user?: Record<string, unknown>;
|
|
158
|
+
/** 登录时间。 */
|
|
159
|
+
loginTime?: string;
|
|
160
|
+
/** 最后使用时间。 */
|
|
161
|
+
lastUsedTime?: string;
|
|
162
|
+
/** 禅道服务端配置。 */
|
|
163
|
+
serverConfig?: ServerConfig;
|
|
164
|
+
/** 客户端自定义配置。 */
|
|
165
|
+
config?: ZentaoProfileConfig;
|
|
166
|
+
/** 允许上层应用保存额外字段。 */
|
|
167
|
+
[key: string]: unknown;
|
|
168
|
+
}
|
|
169
|
+
/** 运行时返回的 profile,会额外带上 `account@server` 形式的 key。 */
|
|
170
|
+
export interface ZentaoProfileRecord extends ZentaoProfile {
|
|
171
|
+
key: string;
|
|
172
|
+
}
|
|
173
|
+
/** 本地 profile 存储文件或浏览器 localStorage 中的 JSON 结构。 */
|
|
174
|
+
export interface ZentaoProfilesStore {
|
|
175
|
+
/** 当前使用的 profile key。 */
|
|
176
|
+
currentProfile?: string;
|
|
177
|
+
/** 保存的 profile 列表。 */
|
|
178
|
+
profiles: ZentaoProfile[];
|
|
179
|
+
}
|
|
114
180
|
/** 模块动作类型:基础 CRUD 或自定义动作。 */
|
|
115
181
|
export type ModuleActionType = 'list' | 'get' | 'create' | 'update' | 'delete' | 'action';
|
|
116
182
|
/** 模块动作使用的 HTTP 方法;兼容生成定义中的小写方法。 */
|
package/dist/utils/index.d.ts
CHANGED
|
@@ -1,2 +1,6 @@
|
|
|
1
|
+
/** 判断值是否为普通对象(非数组、非 null)。 */
|
|
2
|
+
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
|
3
|
+
/** 将用户传入的站点根地址规范化,兼容误传入 `/api.php/v2` 的场景。 */
|
|
4
|
+
export declare function normalizeSiteUrl(baseUrl: string): string;
|
|
1
5
|
export declare function getNestedValue(obj: unknown, path: string): unknown;
|
|
2
6
|
export declare function asArray<T>(value: T | T[]): T[];
|
package/dist/utils/index.js
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
/** 判断值是否为普通对象(非数组、非 null)。 */
|
|
3
|
+
export function isRecord(value) {
|
|
4
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
/** 将用户传入的站点根地址规范化,兼容误传入 `/api.php/v2` 的场景。 */
|
|
7
|
+
export function normalizeSiteUrl(baseUrl) {
|
|
8
|
+
const trimmed = baseUrl.trim().replace(/\/+$/, '');
|
|
9
|
+
if (!trimmed)
|
|
10
|
+
throw new ZentaoError('E_INVALID_BASE_URL');
|
|
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(/\/+$/, '');
|
|
23
|
+
}
|
|
1
24
|
export function getNestedValue(obj, path) {
|
|
2
25
|
const keys = path.split('.');
|
|
3
26
|
let current = obj;
|
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-
|
|
2
|
-
const fallbackVersion = "0.2.0
|
|
1
|
+
const fallbackBuild = "2026-05-25T12:42:48.013Z";
|
|
2
|
+
const fallbackVersion = "0.2.0";
|
|
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
|
|
3
|
+
"version": "0.2.0",
|
|
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/
|
|
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":
|
|
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": [
|
|
@@ -41,19 +50,25 @@
|
|
|
41
50
|
"build": "rm -rf dist && tsc -p tsconfig.json && bun run scripts/build-browser.ts",
|
|
42
51
|
"smoke:node": "node scripts/smoke-node.mjs",
|
|
43
52
|
"smoke:package": "bun run scripts/smoke-package.ts",
|
|
53
|
+
"docs:reference": "typedoc",
|
|
54
|
+
"docs:zentao-api": "bun run scripts/generate-zentao-api-docs.ts",
|
|
55
|
+
"docs:generate": "bun run docs:reference && bun run docs:zentao-api",
|
|
56
|
+
"docs:dev": "bun run docs:generate && vitepress dev docs",
|
|
57
|
+
"docs:build": "bun run docs:generate && vitepress build docs",
|
|
58
|
+
"docs:preview": "bun run docs:build && vitepress preview docs",
|
|
44
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",
|
|
45
60
|
"prepublishOnly": "bun run check"
|
|
46
61
|
},
|
|
47
|
-
"dependencies": {
|
|
48
|
-
"turndown": "^7.2.0"
|
|
49
|
-
},
|
|
50
62
|
"devDependencies": {
|
|
51
63
|
"@types/bun": "^1.3.13",
|
|
52
64
|
"@types/node": "^24.10.0",
|
|
53
|
-
"
|
|
54
|
-
"
|
|
65
|
+
"typedoc": "^0.28.19",
|
|
66
|
+
"typedoc-plugin-markdown": "^4.11.0",
|
|
67
|
+
"typedoc-vitepress-theme": "^1.1.2",
|
|
68
|
+
"typescript": "^5.7.0",
|
|
69
|
+
"vitepress": "^1.6.4"
|
|
55
70
|
},
|
|
56
71
|
"engines": {
|
|
57
72
|
"node": ">=18"
|
|
58
73
|
}
|
|
59
|
-
}
|
|
74
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from '../index.js';
|