zentao-api 0.6.6 → 0.6.8
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 +41 -2
- package/dist/browser/zentao-api.global.js +21 -21
- package/dist/client/index.d.ts +45 -5
- package/dist/client/index.js +131 -18
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/misc/errors.d.ts +6 -1
- package/dist/misc/errors.js +9 -1
- package/dist/misc/zentao-version.d.ts +11 -0
- package/dist/misc/zentao-version.js +35 -0
- package/dist/modules/define.d.ts +4 -3
- package/dist/modules/define.js +4 -3
- package/dist/modules/generated.d.ts +228 -0
- package/dist/modules/generated.js +228 -0
- package/dist/modules/override.d.ts +2 -0
- package/dist/modules/override.js +3 -0
- package/dist/modules/query.d.ts +13 -8
- package/dist/modules/query.js +25 -11
- package/dist/modules/registry-store.js +6 -0
- package/dist/profiles/file-lock.d.ts +6 -0
- package/dist/profiles/file-lock.js +127 -0
- package/dist/profiles/index.d.ts +11 -4
- package/dist/profiles/index.js +134 -79
- package/dist/request/index.d.ts +3 -0
- package/dist/request/index.js +47 -11
- package/dist/types/client.d.ts +10 -0
- package/dist/types/module.d.ts +32 -2
- package/dist/types/options.d.ts +8 -0
- package/dist/types/profile.d.ts +14 -8
- package/dist/types/response.d.ts +9 -0
- package/dist/version.js +2 -2
- package/package.json +1 -1
package/dist/profiles/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ZentaoError } from '../misc/errors.js';
|
|
2
2
|
import { isNodeRuntime } from '../misc/environment.js';
|
|
3
3
|
import { isRecord, normalizeSiteUrl } from '../utils/index.js';
|
|
4
|
+
import { withProfileFileLock } from './file-lock.js';
|
|
4
5
|
/**
|
|
5
6
|
* 浏览器环境下用于在 `localStorage` 中保存 profile 数据的 key。
|
|
6
7
|
*
|
|
@@ -21,23 +22,48 @@ function nowString() {
|
|
|
21
22
|
function importNodeModule(specifier) {
|
|
22
23
|
return import(specifier);
|
|
23
24
|
}
|
|
24
|
-
//
|
|
25
|
-
// 避免并发 `addProfile`/`switchProfile` 出现 lost update(写文件本身是原子
|
|
26
|
-
// rename,但 read→modify→write 之间没有跨步保护)。跨进程并发不在保证范围内。
|
|
25
|
+
// 所有 read-modify-write 都先进入实例队列,再取得文件锁或浏览器 Web Lock。
|
|
27
26
|
let storeMutex = Promise.resolve();
|
|
28
27
|
function withStoreMutex(operation) {
|
|
29
|
-
const
|
|
28
|
+
const run = () => withStorageErrors(async () => {
|
|
29
|
+
if (isNodeRuntime())
|
|
30
|
+
return withProfileFileLock(await getProfileFilePath(), operation);
|
|
31
|
+
const locks = globalThis.navigator?.locks;
|
|
32
|
+
// ponytail: 没有 Web Locks 时仅保证实例内串行;需要跨标签保证的应用应使用支持 Web Locks 的环境。
|
|
33
|
+
if (!locks)
|
|
34
|
+
return operation();
|
|
35
|
+
const controller = new AbortController();
|
|
36
|
+
const timeout = setTimeout(() => controller.abort(), 5000);
|
|
37
|
+
try {
|
|
38
|
+
return await locks.request(`${ZENTAO_PROFILES_STORAGE_KEY}:write`, { signal: controller.signal }, () => {
|
|
39
|
+
clearTimeout(timeout);
|
|
40
|
+
return operation();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
clearTimeout(timeout);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const next = storeMutex.then(run, run);
|
|
30
48
|
storeMutex = next.catch(() => undefined);
|
|
31
49
|
return next;
|
|
32
50
|
}
|
|
33
|
-
function
|
|
51
|
+
async function withStorageErrors(operation) {
|
|
34
52
|
try {
|
|
35
|
-
return
|
|
53
|
+
return await operation();
|
|
36
54
|
}
|
|
37
|
-
catch {
|
|
38
|
-
|
|
55
|
+
catch (error) {
|
|
56
|
+
if (error instanceof ZentaoError)
|
|
57
|
+
throw error;
|
|
58
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE', undefined, error);
|
|
39
59
|
}
|
|
40
60
|
}
|
|
61
|
+
function getBrowserStorage() {
|
|
62
|
+
const storage = globalThis.localStorage;
|
|
63
|
+
if (!storage)
|
|
64
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
65
|
+
return storage;
|
|
66
|
+
}
|
|
41
67
|
async function getProfileFilePath() {
|
|
42
68
|
const path = await importNodeModule('node:path');
|
|
43
69
|
const home = process.env.HOME
|
|
@@ -60,30 +86,31 @@ function normalizeProfile(profile) {
|
|
|
60
86
|
throw new ZentaoError('E_INVALID_PROFILE');
|
|
61
87
|
}
|
|
62
88
|
const token = profile.token.trim();
|
|
63
|
-
|
|
89
|
+
const account = profile.account.trim();
|
|
90
|
+
if (!token || !account)
|
|
64
91
|
throw new ZentaoError('E_INVALID_PROFILE');
|
|
65
92
|
const copy = cloneJson(profile);
|
|
66
93
|
delete copy.key;
|
|
67
94
|
return {
|
|
68
95
|
...copy,
|
|
69
96
|
server: normalizeSiteUrl(profile.server),
|
|
70
|
-
account
|
|
97
|
+
account,
|
|
71
98
|
token,
|
|
72
99
|
};
|
|
73
100
|
}
|
|
74
101
|
function normalizeStore(raw) {
|
|
75
|
-
if (!isRecord(raw))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
102
|
+
if (!isRecord(raw) || !Array.isArray(raw.profiles)
|
|
103
|
+
|| (raw.currentProfile !== undefined && typeof raw.currentProfile !== 'string')) {
|
|
104
|
+
throw new ZentaoError('E_PROFILE_STORAGE_INVALID');
|
|
105
|
+
}
|
|
106
|
+
const profiles = raw.profiles.flatMap((profile) => {
|
|
107
|
+
try {
|
|
108
|
+
return [normalizeProfile(profile)];
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
});
|
|
87
114
|
const currentProfile = typeof raw.currentProfile === 'string' ? raw.currentProfile : undefined;
|
|
88
115
|
return currentProfile ? { currentProfile, profiles } : { profiles };
|
|
89
116
|
}
|
|
@@ -95,54 +122,50 @@ function parseStore(text) {
|
|
|
95
122
|
throw new ZentaoError('E_PROFILE_STORAGE_INVALID', undefined, error);
|
|
96
123
|
}
|
|
97
124
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
125
|
+
function readStore() {
|
|
126
|
+
return withStorageErrors(async () => {
|
|
127
|
+
if (isNodeRuntime()) {
|
|
128
|
+
const fs = await importNodeModule('node:fs/promises');
|
|
129
|
+
const file = await getProfileFilePath();
|
|
130
|
+
try {
|
|
131
|
+
return parseStore(await fs.readFile(file, 'utf8'));
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
if (error.code === 'ENOENT') {
|
|
135
|
+
return { profiles: [] };
|
|
136
|
+
}
|
|
137
|
+
throw error;
|
|
108
138
|
}
|
|
109
|
-
throw error;
|
|
110
139
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
115
|
-
}
|
|
116
|
-
const text = storage.getItem(ZENTAO_PROFILES_STORAGE_KEY);
|
|
117
|
-
return text ? parseStore(text) : { profiles: [] };
|
|
140
|
+
const text = getBrowserStorage().getItem(ZENTAO_PROFILES_STORAGE_KEY);
|
|
141
|
+
return text === null ? { profiles: [] } : parseStore(text);
|
|
142
|
+
});
|
|
118
143
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
144
|
+
function writeStore(store) {
|
|
145
|
+
return withStorageErrors(async () => {
|
|
146
|
+
const normalizedStore = normalizeStore(store);
|
|
147
|
+
const text = `${JSON.stringify(normalizedStore, null, 2)}\n`;
|
|
148
|
+
if (isNodeRuntime()) {
|
|
149
|
+
const fs = await importNodeModule('node:fs/promises');
|
|
150
|
+
const path = await importNodeModule('node:path');
|
|
151
|
+
const file = await getProfileFilePath();
|
|
152
|
+
const dir = path.dirname(file);
|
|
153
|
+
const tempFile = path.join(dir, `.zentao.${Date.now()}.${Math.random().toString(36).slice(2)}.tmp`);
|
|
154
|
+
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
155
|
+
await fs.chmod(dir, 0o700).catch(() => undefined);
|
|
156
|
+
try {
|
|
157
|
+
await fs.writeFile(tempFile, text, { encoding: 'utf8', mode: 0o600 });
|
|
158
|
+
await fs.rename(tempFile, file);
|
|
159
|
+
await fs.chmod(file, 0o600).catch(() => undefined);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
await fs.rm(tempFile, { force: true }).catch(() => undefined);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
138
166
|
}
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
const storage = getBrowserStorage();
|
|
142
|
-
if (!storage) {
|
|
143
|
-
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
144
|
-
}
|
|
145
|
-
storage.setItem(ZENTAO_PROFILES_STORAGE_KEY, text);
|
|
167
|
+
getBrowserStorage().setItem(ZENTAO_PROFILES_STORAGE_KEY, text);
|
|
168
|
+
});
|
|
146
169
|
}
|
|
147
170
|
function toRecord(profile) {
|
|
148
171
|
const normalized = normalizeProfile(profile);
|
|
@@ -154,6 +177,15 @@ function toRecord(profile) {
|
|
|
154
177
|
function findProfile(store, profileKey) {
|
|
155
178
|
return store.profiles.find((profile) => getProfileKey(profile) === profileKey);
|
|
156
179
|
}
|
|
180
|
+
function requireProfile(store, profileKey) {
|
|
181
|
+
const key = profileKey ?? store.currentProfile;
|
|
182
|
+
if (!key)
|
|
183
|
+
throw new ZentaoError('E_NO_PROFILE');
|
|
184
|
+
const profile = findProfile(store, key);
|
|
185
|
+
if (!profile)
|
|
186
|
+
throw new ZentaoError('E_PROFILE_NOT_FOUND', { profileKey: key });
|
|
187
|
+
return profile;
|
|
188
|
+
}
|
|
157
189
|
function setFallbackCurrentProfile(store) {
|
|
158
190
|
if (!store.currentProfile || !findProfile(store, store.currentProfile)) {
|
|
159
191
|
const fallback = store.profiles.at(-1);
|
|
@@ -180,7 +212,7 @@ export function getProfileKey(profile) {
|
|
|
180
212
|
* 读取过程不会写回存储;存储中无法解析的条目会被静默忽略,不会影响其余 profile。
|
|
181
213
|
*
|
|
182
214
|
* @returns 当前存储中的所有 profile(带 `key` 字段),文件不存在时返回空数组。
|
|
183
|
-
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID`(存储内容不是合法 JSON
|
|
215
|
+
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID`(存储内容不是合法 JSON 或根结构不合法)或
|
|
184
216
|
* `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问存储)。
|
|
185
217
|
*/
|
|
186
218
|
export async function getAllProfiles() {
|
|
@@ -202,13 +234,18 @@ export async function getProfile(profileKey) {
|
|
|
202
234
|
const profile = findProfile(store, key);
|
|
203
235
|
return profile ? toRecord(profile) : undefined;
|
|
204
236
|
}
|
|
237
|
+
/** 只读恢复与账号切换共用相同的 key 解析和错误语义。 @internal */
|
|
238
|
+
export async function getProfileOrThrow(profileKey) {
|
|
239
|
+
return toRecord(requireProfile(await readStore(), profileKey));
|
|
240
|
+
}
|
|
205
241
|
/**
|
|
206
242
|
* 添加或覆盖一个本地 profile,并把它设置为当前使用的 profile。
|
|
207
243
|
*
|
|
208
244
|
* 行为细节:
|
|
209
245
|
* - 同 key(`account@server`)已存在时会**整体覆盖**而非合并字段。
|
|
210
246
|
* - 写入时会自动补齐 `loginTime` 与 `lastUsedTime`(若调用方未提供则使用当前 ISO 时间)。
|
|
211
|
-
* -
|
|
247
|
+
* - Node.js 通过文件锁保护同主机本地文件系统的 read-modify-write;浏览器在支持 Web Locks 时保护同源上下文,其他环境仅保证实例内串行。
|
|
248
|
+
* - 跨进程或上下文等待锁超过约 5 秒时抛出存储不可用错误,不修改 profile 数据。
|
|
212
249
|
* - 实际写入使用临时文件 + `rename` 的原子方式,并将文件与目录权限收紧到 `0600`/`0700`(Node.js 下)。
|
|
213
250
|
*
|
|
214
251
|
* @param profile - 要写入的 profile,必须至少包含 `server`、`account`、`token`。
|
|
@@ -217,10 +254,17 @@ export async function getProfile(profileKey) {
|
|
|
217
254
|
* `E_INVALID_BASE_URL`、`E_PROFILE_STORAGE_INVALID`、`E_PROFILE_STORAGE_UNAVAILABLE`。
|
|
218
255
|
*/
|
|
219
256
|
export function addProfile(profile) {
|
|
257
|
+
return saveProfile(profile);
|
|
258
|
+
}
|
|
259
|
+
/** 登录只更新会话字段和显式偏好,保留同账号的应用数据。 @internal */
|
|
260
|
+
export function saveLoginProfile(profile) {
|
|
261
|
+
return saveProfile(profile, true);
|
|
262
|
+
}
|
|
263
|
+
function saveProfile(profile, preservePreferences = false) {
|
|
220
264
|
return withStoreMutex(async () => {
|
|
221
265
|
const store = await readStore();
|
|
222
266
|
const timestamp = nowString();
|
|
223
|
-
|
|
267
|
+
let normalized = normalizeProfile({
|
|
224
268
|
...profile,
|
|
225
269
|
loginTime: profile.loginTime ?? timestamp,
|
|
226
270
|
lastUsedTime: profile.lastUsedTime ?? timestamp,
|
|
@@ -228,6 +272,12 @@ export function addProfile(profile) {
|
|
|
228
272
|
const profileKey = getProfileKey(normalized);
|
|
229
273
|
const index = store.profiles.findIndex((item) => getProfileKey(item) === profileKey);
|
|
230
274
|
if (index >= 0) {
|
|
275
|
+
if (preservePreferences) {
|
|
276
|
+
const previous = store.profiles[index];
|
|
277
|
+
const config = { ...(isRecord(previous.config) ? previous.config : {}), ...normalized.config };
|
|
278
|
+
normalized = normalizeProfile({ ...previous, ...profile, ...normalized,
|
|
279
|
+
config: Object.keys(config).length ? config : undefined });
|
|
280
|
+
}
|
|
231
281
|
store.profiles[index] = normalized;
|
|
232
282
|
}
|
|
233
283
|
else {
|
|
@@ -238,10 +288,22 @@ export function addProfile(profile) {
|
|
|
238
288
|
return toRecord(normalized);
|
|
239
289
|
});
|
|
240
290
|
}
|
|
291
|
+
/** 只刷新已有 profile 的服务器配置,不切换当前账号或重建已删除的记录。 @internal */
|
|
292
|
+
export function updateProfileServerConfig(profileKey, serverConfig, fetchedAt) {
|
|
293
|
+
return withStoreMutex(async () => {
|
|
294
|
+
const store = await readStore();
|
|
295
|
+
const profile = findProfile(store, profileKey);
|
|
296
|
+
if (!profile)
|
|
297
|
+
return;
|
|
298
|
+
profile.serverConfig = cloneJson(serverConfig);
|
|
299
|
+
profile.serverConfigFetchedAt = fetchedAt;
|
|
300
|
+
await writeStore(store);
|
|
301
|
+
});
|
|
302
|
+
}
|
|
241
303
|
/**
|
|
242
304
|
* 删除指定 profile。
|
|
243
305
|
*
|
|
244
|
-
* 若被删除的是当前 profile
|
|
306
|
+
* 若被删除的是当前 profile,会回退为最近添加且仍保留的 profile;覆盖或切换已有记录不改变添加顺序。若已无任何 profile,
|
|
245
307
|
* 当前 profile 会被清空。操作同样通过进程内串行锁保护。
|
|
246
308
|
*
|
|
247
309
|
* @param profileKey - 要删除的 profile key。
|
|
@@ -275,16 +337,9 @@ export function deleteProfile(profileKey) {
|
|
|
275
337
|
export function switchProfile(profileKey) {
|
|
276
338
|
return withStoreMutex(async () => {
|
|
277
339
|
const store = await readStore();
|
|
278
|
-
const
|
|
279
|
-
if (!key) {
|
|
280
|
-
throw new ZentaoError('E_NO_PROFILE');
|
|
281
|
-
}
|
|
282
|
-
const profile = findProfile(store, key);
|
|
283
|
-
if (!profile) {
|
|
284
|
-
throw new ZentaoError('E_PROFILE_NOT_FOUND', { profileKey: key });
|
|
285
|
-
}
|
|
340
|
+
const profile = requireProfile(store, profileKey);
|
|
286
341
|
profile.lastUsedTime = nowString();
|
|
287
|
-
store.currentProfile =
|
|
342
|
+
store.currentProfile = getProfileKey(profile);
|
|
288
343
|
await writeStore(store);
|
|
289
344
|
return toRecord(profile);
|
|
290
345
|
});
|
package/dist/request/index.d.ts
CHANGED
|
@@ -27,6 +27,9 @@ export type RequestResultFor<Name extends BuiltinRequestName> = ActionMetaOf<Nam
|
|
|
27
27
|
* 选项优先级为:本次调用 options > 全局 options > 客户端默认值。
|
|
28
28
|
* 当响应 `status` 为 `"fail"` 时,默认按原样返回;若 `options.throwOnFail`
|
|
29
29
|
* 或全局 `throwOnFail` 为真,则改为抛出 `E_API_FAILED`。
|
|
30
|
+
* 请求前按 Action 的 `minVersion` 检查服务器版本;全局 `version` 可避免配置探测,
|
|
31
|
+
* `forceRefreshConfig` 则强制使用实际版本。无法获取配置时默认抛错,
|
|
32
|
+
* `skipVersionCheckOnConfigError` 可允许本次跳过检查,但不能忽略明确的版本不匹配。
|
|
30
33
|
*
|
|
31
34
|
* 对 `update` 动作,当 `options.autoFill` 或全局 `autoFill` 为真时,会先 GET 当前对象,
|
|
32
35
|
* 用现值补齐用户未显式传入的 body 字段后再 PUT,避免禅道覆盖未提交字段。详见 {@link RequestOptions.autoFill}。
|
package/dist/request/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { ZentaoError } from '../misc/errors.js';
|
|
1
|
+
import { isZentaoConfigFetchError, ZentaoError } from '../misc/errors.js';
|
|
2
|
+
import { parseZentaoVersion, supportsZentaoVersion } from '../misc/zentao-version.js';
|
|
2
3
|
import { getGlobalOptions } from '../misc/global-options.js';
|
|
3
4
|
import { getModule, getModuleAction } from '../modules/registry.js';
|
|
4
5
|
import { extractPager, extractResult, resolveActionRequest } from '../modules/resolve.js';
|
|
@@ -57,17 +58,17 @@ function getExplicitDataKeys(data) {
|
|
|
57
58
|
* 字段归属判断同时覆盖平铺 `params` 字段与 `params.data` 中的字段;只有 schema 中声明、
|
|
58
59
|
* 用户未传且当前对象存在的字段才会被补齐,避免覆盖用户本次想修改的字段。
|
|
59
60
|
*/
|
|
60
|
-
async function autoFillUpdateParams(module, action, params, options) {
|
|
61
|
+
async function autoFillUpdateParams(module, action, params, options, version) {
|
|
61
62
|
const properties = action.requestBody?.schema?.properties;
|
|
62
63
|
const getAction = module.actions.find((candidate) => candidate.type === 'get' && candidate.path === action.path);
|
|
63
64
|
if (!properties || !getAction)
|
|
64
65
|
return params;
|
|
65
|
-
const current = (await
|
|
66
|
+
const current = (await requestInternal(`${module.name}/${getAction.name}`, params, {
|
|
66
67
|
client: options.client,
|
|
67
68
|
timeout: options.timeout,
|
|
68
69
|
insecure: options.insecure,
|
|
69
70
|
throwOnFail: true,
|
|
70
|
-
})).data;
|
|
71
|
+
}, version)).data;
|
|
71
72
|
if (!isRecord(current))
|
|
72
73
|
return params;
|
|
73
74
|
const explicitDataKeys = getExplicitDataKeys(params.data);
|
|
@@ -172,6 +173,10 @@ function normalizeResponse(command, raw, options) {
|
|
|
172
173
|
return response;
|
|
173
174
|
}
|
|
174
175
|
export async function request(name, params = {}, options = {}) {
|
|
176
|
+
return requestInternal(name, params, options);
|
|
177
|
+
}
|
|
178
|
+
/** version 为 undefined 时解析一次,null 表示本次调用已选择跳过版本检查。 */
|
|
179
|
+
async function requestInternal(name, params, options, version) {
|
|
175
180
|
const globals = getGlobalOptions();
|
|
176
181
|
const client = options.client ?? globals.client;
|
|
177
182
|
if (!client) {
|
|
@@ -195,21 +200,52 @@ export async function request(name, params = {}, options = {}) {
|
|
|
195
200
|
if (!action) {
|
|
196
201
|
throw new ZentaoError('E_INVALID_ACTION', { module: moduleName, action: actionName });
|
|
197
202
|
}
|
|
203
|
+
if (version === undefined) {
|
|
204
|
+
if (!options.forceRefreshConfig && globals.version !== undefined) {
|
|
205
|
+
version = globals.version;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
try {
|
|
209
|
+
version = (await client.getZentaoConfig({
|
|
210
|
+
forceRefresh: options.forceRefreshConfig,
|
|
211
|
+
timeout: options.timeout ?? globals.timeout,
|
|
212
|
+
insecure: options.insecure ?? globals.insecure,
|
|
213
|
+
})).version;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (!(options.skipVersionCheckOnConfigError ?? globals.skipVersionCheckOnConfigError)
|
|
217
|
+
|| !isZentaoConfigFetchError(error))
|
|
218
|
+
throw error;
|
|
219
|
+
version = null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (version !== null && !supportsZentaoVersion(parseZentaoVersion(version), action.minVersion)) {
|
|
224
|
+
throw new ZentaoError('E_UNSUPPORTED_ZENTAO_VERSION', {
|
|
225
|
+
action: `${module.name}/${action.name}`,
|
|
226
|
+
version,
|
|
227
|
+
minVersion: action.minVersion.join(', '),
|
|
228
|
+
}, { action: `${module.name}/${action.name}`, version, minVersion: action.minVersion });
|
|
229
|
+
}
|
|
198
230
|
const finalParams = action.type === 'update' && (options.autoFill ?? globals.autoFill)
|
|
199
|
-
? await autoFillUpdateParams(module, action, mergedParams, options)
|
|
231
|
+
? await autoFillUpdateParams(module, action, mergedParams, { ...options, client }, version)
|
|
200
232
|
: mergedParams;
|
|
201
233
|
const command = resolveActionRequest(module, actionName, finalParams);
|
|
202
234
|
const preparedBody = await prepareActionBody(command, {
|
|
203
235
|
maxUploadBytes: options.maxUploadBytes,
|
|
204
236
|
});
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
query: command.query,
|
|
208
|
-
body: preparedBody.body,
|
|
209
|
-
bodyType: preparedBody.bodyType,
|
|
237
|
+
const requestOptions = {
|
|
238
|
+
...preparedBody,
|
|
210
239
|
timeout: options.timeout ?? globals.timeout,
|
|
211
240
|
insecure: options.insecure ?? globals.insecure,
|
|
212
|
-
}
|
|
241
|
+
};
|
|
242
|
+
const raw = command.action.request
|
|
243
|
+
? await command.action.request({ request: command, ...requestOptions, client, options })
|
|
244
|
+
: await client.request(command.path, {
|
|
245
|
+
method: String(command.action.method).toUpperCase(),
|
|
246
|
+
query: command.query,
|
|
247
|
+
...requestOptions,
|
|
248
|
+
});
|
|
213
249
|
if (options.raw) {
|
|
214
250
|
return raw;
|
|
215
251
|
}
|
package/dist/types/client.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** 从本地 profile 恢复客户端时的选项。 */
|
|
2
|
+
export interface FromProfileOptions {
|
|
3
|
+
/** 是否切换为当前 profile 并更新使用时间,默认 true;false 时恢复过程只读存储。 */
|
|
4
|
+
activate?: boolean;
|
|
5
|
+
}
|
|
1
6
|
/** 创建 {@link ZentaoClient} 时使用的配置。 */
|
|
2
7
|
export interface ZentaoClientOptions {
|
|
3
8
|
/** 禅道站点根地址,例如 `https://zentao.example.com`;SDK 会自动拼接 `/api.php/v2`。 */
|
|
@@ -36,3 +41,8 @@ export interface ClientRequestOptions {
|
|
|
36
41
|
/** 单次请求 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
37
42
|
insecure?: boolean;
|
|
38
43
|
}
|
|
44
|
+
/** {@link ZentaoClient.getZentaoConfig} 的选项。 */
|
|
45
|
+
export interface GetZentaoConfigOptions extends Pick<ClientRequestOptions, 'timeout' | 'insecure' | 'signal'> {
|
|
46
|
+
/** 忽略已有缓存并重新获取配置,默认 false;同一客户端的并发获取会合并。 */
|
|
47
|
+
forceRefresh?: boolean;
|
|
48
|
+
}
|
package/dist/types/module.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type {
|
|
1
|
+
import type { ZentaoClient } from '../client/index.js';
|
|
2
|
+
import type { ClientRequestBodyType, HttpMethod } from './client.js';
|
|
3
|
+
import type { RequestOptions, RequestProcessOptions } from './options.js';
|
|
3
4
|
import type { Pager } from './response.js';
|
|
4
5
|
/** 模块动作类型:基础 CRUD 或自定义动作。 */
|
|
5
6
|
export type ModuleActionType = 'list' | 'get' | 'create' | 'update' | 'delete' | 'action';
|
|
@@ -86,10 +87,32 @@ export type ModuleActionResultFieldMap = Readonly<Record<string, string>>;
|
|
|
86
87
|
* @param options 本次请求的数据处理选项;直接调用 getter 时可以省略。
|
|
87
88
|
*/
|
|
88
89
|
export type ModuleActionGetterFn<T, O = RequestProcessOptions> = (data: unknown, params: Record<string, unknown>, options?: O) => T;
|
|
90
|
+
/**
|
|
91
|
+
* 自定义动作的请求发送逻辑,在版本检查、参数解析和请求体准备完成后调用。
|
|
92
|
+
* 返回原始响应数据,后续仍按动作定义提取结果、分页并应用请求选项;抛出的错误原样传递。
|
|
93
|
+
*/
|
|
94
|
+
export type ModuleActionRequestCallback = (info: {
|
|
95
|
+
/** 解析后的路径、查询参数、请求体数据及动作定义。 */
|
|
96
|
+
request: ModuleActionRequest;
|
|
97
|
+
/** 准备好的请求体,例如 JSON 对象或上传用的 FormData。 */
|
|
98
|
+
body: unknown;
|
|
99
|
+
/** 请求体序列化方式,可直接传给客户端。 */
|
|
100
|
+
bodyType?: ClientRequestBodyType;
|
|
101
|
+
/** 本次请求的超时选项,未指定时回落到全局选项。 */
|
|
102
|
+
timeout?: number;
|
|
103
|
+
/** 本次请求的 TLS 选项,未指定时回落到全局选项。 */
|
|
104
|
+
insecure?: boolean;
|
|
105
|
+
/** 本次调用选定的客户端。 */
|
|
106
|
+
client: ZentaoClient;
|
|
107
|
+
/** 调用方传入的原始请求选项。 */
|
|
108
|
+
options: RequestOptions;
|
|
109
|
+
}) => Promise<unknown>;
|
|
89
110
|
/** 禅道模块中的单个 API 动作定义。 */
|
|
90
111
|
export interface ModuleAction {
|
|
91
112
|
/** 动作名称,例如 `list`、`get`、`close`。 */
|
|
92
113
|
name: ModuleActionName;
|
|
114
|
+
/** 各系列支持的最低禅道正式版本,例如 `['22.0', 'biz13.0', 'max8.0', 'ipd5.0']`;不能为空或重复系列,未列出的系列不支持。 */
|
|
115
|
+
minVersion: readonly string[];
|
|
93
116
|
/** 动作类型,决定高阶 request 的路径/参数解析策略,并在 `method`、`resultType` 省略时作为推导依据。 */
|
|
94
117
|
type: ModuleActionType;
|
|
95
118
|
/** 面向用户展示的动作名称。 */
|
|
@@ -126,6 +149,8 @@ export interface ModuleAction {
|
|
|
126
149
|
* 字符串为字段路径(支持 `a.b` 嵌套)、对象为字段映射、函数则接收原始响应与调用参数。
|
|
127
150
|
*/
|
|
128
151
|
resultGetter?: string | ModuleActionResultFieldMap | ModuleActionGetterFn<unknown>;
|
|
152
|
+
/** 自定义请求回调;省略时通过 ZentaoClient.request 发送请求,返回值沿用相同的响应处理流程。 */
|
|
153
|
+
request?: ModuleActionRequestCallback;
|
|
129
154
|
}
|
|
130
155
|
/** 内置模块名称,同时允许用户扩展自定义模块名。 */
|
|
131
156
|
export type ModuleName = 'user' | 'program' | 'product' | 'project' | 'execution' | 'productplan' | 'story' | 'epic' | 'requirement' | 'bug' | 'testcase' | 'task' | 'feedback' | 'ticket' | 'system' | 'build' | 'testtask' | 'release' | 'file' | (string & {});
|
|
@@ -140,6 +165,11 @@ export interface ModuleDefinition {
|
|
|
140
165
|
/** 模块支持的动作集合。 */
|
|
141
166
|
actions: readonly ModuleAction[];
|
|
142
167
|
}
|
|
168
|
+
/** 注册库查询选项;不传版本时返回完整的当前定义,不使用全局版本。 */
|
|
169
|
+
export interface ModuleQueryOptions {
|
|
170
|
+
/** 仅返回该禅道版本支持的动作,以及至少含有一个支持动作的模块。 */
|
|
171
|
+
version?: string;
|
|
172
|
+
}
|
|
143
173
|
/** 将模块动作和参数解析后的可执行请求描述。 */
|
|
144
174
|
export interface ModuleActionRequest {
|
|
145
175
|
/** 模块名称。 */
|
package/dist/types/options.d.ts
CHANGED
|
@@ -2,6 +2,10 @@ import type { ZentaoClient } from '../client/index.js';
|
|
|
2
2
|
import type { ProcessListOptions, ProcessSingleOptions } from './data.js';
|
|
3
3
|
/** SDK 进程级全局默认选项,供高阶 {@link request} 调用复用。 */
|
|
4
4
|
export interface GlobalOptions {
|
|
5
|
+
/** 当前禅道正式版本,例如 `biz13.5`;普通高阶请求直接使用,单次强制刷新时使用实际版本。不会自动写入 profile。 */
|
|
6
|
+
version?: string;
|
|
7
|
+
/** 配置网络或响应获取失败时,允许登录继续或高阶请求跳过版本检查,默认 false;不忽略版本不匹配、格式错误或存储错误。 */
|
|
8
|
+
skipVersionCheckOnConfigError?: boolean;
|
|
5
9
|
/** 默认客户端;通常由 `ZentaoClient.init()` 设置。 */
|
|
6
10
|
client?: ZentaoClient;
|
|
7
11
|
/** 默认每页记录数,会映射到模块动作的 `recPerPage` 参数。 */
|
|
@@ -25,6 +29,10 @@ export interface GlobalOptions {
|
|
|
25
29
|
}
|
|
26
30
|
/** 高阶 `request("moduleName")` / `request("moduleName/methodName")` / `request("moduleName/<objectID>")` 的单次调用选项。 */
|
|
27
31
|
export interface RequestOptions extends ProcessListOptions {
|
|
32
|
+
/** 强制刷新服务器配置并用实际版本校验本次请求,优先于全局 version;不会改写全局版本。 */
|
|
33
|
+
forceRefreshConfig?: boolean;
|
|
34
|
+
/** 配置网络或响应获取失败时跳过本次版本检查;优先于全局设置,默认 false,不忽略版本不匹配或格式错误。 */
|
|
35
|
+
skipVersionCheckOnConfigError?: boolean;
|
|
28
36
|
/** 本次调用使用的客户端;优先级高于全局客户端。 */
|
|
29
37
|
client?: ZentaoClient;
|
|
30
38
|
/** 本次调用使用的每页记录数,优先级高于全局 `recPerPage`。 */
|
package/dist/types/profile.d.ts
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import type { ServerConfig } from './response.js';
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* 保存到本地 profile 中的客户端偏好配置。
|
|
4
|
+
* SDK 自动恢复 `timeout` / `insecure`;其余字段仅供上层应用读取和解释。
|
|
5
|
+
* 自定义值应使用 JSON 数据,不保留 Date、Map 等类型信息,不支持 BigInt 或循环对象。
|
|
6
|
+
*/
|
|
3
7
|
export interface ZentaoProfileConfig {
|
|
4
8
|
/** 默认输出格式,供 CLI 等上层应用复用。 */
|
|
5
9
|
defaultOutputFormat?: 'markdown' | 'json' | 'raw';
|
|
6
|
-
/**
|
|
10
|
+
/** 上层应用的界面语言,SDK 不自动应用。 */
|
|
7
11
|
lang?: string;
|
|
8
|
-
/**
|
|
12
|
+
/** 上层应用的默认分页大小;SDK 请求分页使用 `recPerPage` 选项。 */
|
|
9
13
|
defaultRecPerPage?: number;
|
|
10
14
|
/** 是否跳过 TLS 证书验证;仅 Node.js 运行时支持。 */
|
|
11
15
|
insecure?: boolean;
|
|
12
16
|
/** 请求超时时间,单位毫秒。 */
|
|
13
17
|
timeout?: number;
|
|
14
|
-
/**
|
|
18
|
+
/** 上层应用是否在批量操作出错时停止执行后续操作。 */
|
|
15
19
|
batchFailFast?: boolean;
|
|
16
|
-
/** JSON
|
|
20
|
+
/** 上层应用格式化 JSON 时是否添加缩进。 */
|
|
17
21
|
jsonPretty?: boolean;
|
|
18
|
-
/**
|
|
22
|
+
/** 上层应用的模块级分页偏好,SDK 不自动应用。 */
|
|
19
23
|
pagers?: Record<string, number>;
|
|
20
|
-
/**
|
|
24
|
+
/** 允许上层应用保存 JSON 格式的自定义配置。 */
|
|
21
25
|
[key: string]: unknown;
|
|
22
26
|
}
|
|
23
27
|
/** 本地持久化的禅道账号 profile。 */
|
|
@@ -36,9 +40,11 @@ export interface ZentaoProfile {
|
|
|
36
40
|
lastUsedTime?: string;
|
|
37
41
|
/** 禅道服务端配置。 */
|
|
38
42
|
serverConfig?: ServerConfig;
|
|
43
|
+
/** 成功从 `?mode=getconfig` 获取配置的本地 ISO 时间;缺失时缓存需要刷新。 */
|
|
44
|
+
serverConfigFetchedAt?: string;
|
|
39
45
|
/** 客户端自定义配置。 */
|
|
40
46
|
config?: ZentaoProfileConfig;
|
|
41
|
-
/**
|
|
47
|
+
/** 允许上层应用保存 JSON 格式的额外字段,不保留 Date、Map 等类型信息,不支持 BigInt 或循环对象。 */
|
|
42
48
|
[key: string]: unknown;
|
|
43
49
|
}
|
|
44
50
|
/** 运行时返回的 profile,会额外带上 `account@server` 形式的 key。 */
|
package/dist/types/response.d.ts
CHANGED
|
@@ -58,6 +58,13 @@ export interface LoginResponse extends ApiResponse {
|
|
|
58
58
|
}
|
|
59
59
|
/** 禅道 `?mode=getconfig` 返回的服务端配置。 */
|
|
60
60
|
export interface ServerConfig {
|
|
61
|
+
/**
|
|
62
|
+
* 禅道版本,不同系列以不同的前缀表示,下面为例子:
|
|
63
|
+
* - `22.5`:开源版 22.5
|
|
64
|
+
* - `biz13.5`:企业版 13.5
|
|
65
|
+
* - `max8.5`:旗舰版 8.5
|
|
66
|
+
* - `ipd5.5`:IPD 5.5
|
|
67
|
+
*/
|
|
61
68
|
version: string;
|
|
62
69
|
systemMode: string;
|
|
63
70
|
sprintConcept: string;
|
|
@@ -67,4 +74,6 @@ export interface ServerConfig {
|
|
|
67
74
|
methodVar: string;
|
|
68
75
|
viewVar: string;
|
|
69
76
|
sessionVar: string;
|
|
77
|
+
/** 保留服务端返回的其他配置字段。 */
|
|
78
|
+
[key: string]: unknown;
|
|
70
79
|
}
|
package/dist/version.js
CHANGED