zentao-api 0.6.7 → 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 +13 -1
- package/dist/browser/zentao-api.global.js +17 -17
- package/dist/client/index.d.ts +23 -7
- package/dist/client/index.js +31 -17
- package/dist/misc/errors.d.ts +1 -1
- package/dist/misc/errors.js +1 -1
- package/dist/modules/registry-store.js +3 -0
- package/dist/profiles/file-lock.d.ts +6 -0
- package/dist/profiles/file-lock.js +127 -0
- package/dist/profiles/index.d.ts +8 -3
- package/dist/profiles/index.js +122 -79
- package/dist/request/index.js +10 -6
- package/dist/types/client.d.ts +5 -0
- package/dist/types/module.d.ts +25 -2
- package/dist/types/profile.d.ts +12 -8
- 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 {
|
|
@@ -253,7 +303,7 @@ export function updateProfileServerConfig(profileKey, serverConfig, fetchedAt) {
|
|
|
253
303
|
/**
|
|
254
304
|
* 删除指定 profile。
|
|
255
305
|
*
|
|
256
|
-
* 若被删除的是当前 profile
|
|
306
|
+
* 若被删除的是当前 profile,会回退为最近添加且仍保留的 profile;覆盖或切换已有记录不改变添加顺序。若已无任何 profile,
|
|
257
307
|
* 当前 profile 会被清空。操作同样通过进程内串行锁保护。
|
|
258
308
|
*
|
|
259
309
|
* @param profileKey - 要删除的 profile key。
|
|
@@ -287,16 +337,9 @@ export function deleteProfile(profileKey) {
|
|
|
287
337
|
export function switchProfile(profileKey) {
|
|
288
338
|
return withStoreMutex(async () => {
|
|
289
339
|
const store = await readStore();
|
|
290
|
-
const
|
|
291
|
-
if (!key) {
|
|
292
|
-
throw new ZentaoError('E_NO_PROFILE');
|
|
293
|
-
}
|
|
294
|
-
const profile = findProfile(store, key);
|
|
295
|
-
if (!profile) {
|
|
296
|
-
throw new ZentaoError('E_PROFILE_NOT_FOUND', { profileKey: key });
|
|
297
|
-
}
|
|
340
|
+
const profile = requireProfile(store, profileKey);
|
|
298
341
|
profile.lastUsedTime = nowString();
|
|
299
|
-
store.currentProfile =
|
|
342
|
+
store.currentProfile = getProfileKey(profile);
|
|
300
343
|
await writeStore(store);
|
|
301
344
|
return toRecord(profile);
|
|
302
345
|
});
|
package/dist/request/index.js
CHANGED
|
@@ -234,14 +234,18 @@ async function requestInternal(name, params, options, version) {
|
|
|
234
234
|
const preparedBody = await prepareActionBody(command, {
|
|
235
235
|
maxUploadBytes: options.maxUploadBytes,
|
|
236
236
|
});
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
query: command.query,
|
|
240
|
-
body: preparedBody.body,
|
|
241
|
-
bodyType: preparedBody.bodyType,
|
|
237
|
+
const requestOptions = {
|
|
238
|
+
...preparedBody,
|
|
242
239
|
timeout: options.timeout ?? globals.timeout,
|
|
243
240
|
insecure: options.insecure ?? globals.insecure,
|
|
244
|
-
}
|
|
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
|
+
});
|
|
245
249
|
if (options.raw) {
|
|
246
250
|
return raw;
|
|
247
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`。 */
|
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,6 +87,26 @@ 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`。 */
|
|
@@ -128,6 +149,8 @@ export interface ModuleAction {
|
|
|
128
149
|
* 字符串为字段路径(支持 `a.b` 嵌套)、对象为字段映射、函数则接收原始响应与调用参数。
|
|
129
150
|
*/
|
|
130
151
|
resultGetter?: string | ModuleActionResultFieldMap | ModuleActionGetterFn<unknown>;
|
|
152
|
+
/** 自定义请求回调;省略时通过 ZentaoClient.request 发送请求,返回值沿用相同的响应处理流程。 */
|
|
153
|
+
request?: ModuleActionRequestCallback;
|
|
131
154
|
}
|
|
132
155
|
/** 内置模块名称,同时允许用户扩展自定义模块名。 */
|
|
133
156
|
export type ModuleName = 'user' | 'program' | 'product' | 'project' | 'execution' | 'productplan' | 'story' | 'epic' | 'requirement' | 'bug' | 'testcase' | 'task' | 'feedback' | 'ticket' | 'system' | 'build' | 'testtask' | 'release' | 'file' | (string & {});
|
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。 */
|
|
@@ -40,7 +44,7 @@ export interface ZentaoProfile {
|
|
|
40
44
|
serverConfigFetchedAt?: string;
|
|
41
45
|
/** 客户端自定义配置。 */
|
|
42
46
|
config?: ZentaoProfileConfig;
|
|
43
|
-
/**
|
|
47
|
+
/** 允许上层应用保存 JSON 格式的额外字段,不保留 Date、Map 等类型信息,不支持 BigInt 或循环对象。 */
|
|
44
48
|
[key: string]: unknown;
|
|
45
49
|
}
|
|
46
50
|
/** 运行时返回的 profile,会额外带上 `account@server` 形式的 key。 */
|
package/dist/version.js
CHANGED