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/client/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ClientRequestOptions, GetZentaoConfigOptions, ServerConfig, ZentaoClientOptions } from '../types/index.js';
|
|
1
|
+
import type { ClientRequestOptions, FromProfileOptions, GetZentaoConfigOptions, ServerConfig, ZentaoClientOptions } from '../types/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* 禅道 API 客户端,封装一次次原始 HTTP 调用。
|
|
4
4
|
*
|
|
@@ -69,10 +69,21 @@ export declare class ZentaoClient {
|
|
|
69
69
|
responseType: 'blob';
|
|
70
70
|
}): Promise<Blob>;
|
|
71
71
|
request<T = unknown>(path: string, options?: ClientRequestOptions): Promise<T>;
|
|
72
|
-
/** API 与站点配置共用的传输层;配置请求不传入 Token。 */
|
|
73
|
-
private fetchUrl;
|
|
74
72
|
/**
|
|
75
|
-
*
|
|
73
|
+
* 使用完整 URL 发起请求,复用 API 与站点配置的传输层。
|
|
74
|
+
*
|
|
75
|
+
* @param url - 完整请求 URL,包含所需的查询参数。
|
|
76
|
+
* @param options - 单次请求选项;其中 `query` 需由调用方预先拼入 `url`。
|
|
77
|
+
* @param token - 显式注入的 Token;省略时不自动使用实例保存的 Token。
|
|
78
|
+
* @param fetchOptions - 原生 fetch 的缓存与凭据选项。
|
|
79
|
+
* @returns 按 `options.responseType` 解析的响应体,默认优先 JSON,失败后返回文本。
|
|
80
|
+
* @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
|
|
81
|
+
*/
|
|
82
|
+
fetch(url: string, options: ClientRequestOptions, token?: string, fetchOptions?: Pick<RequestInit, 'cache' | 'credentials'>): Promise<unknown>;
|
|
83
|
+
/**
|
|
84
|
+
* 匿名获取禅道站点 `/?mode=getconfig` 配置,只需站点地址,无需登录或 profile。
|
|
85
|
+
*
|
|
86
|
+
* 不发送 API Token,浏览器请求显式省略 Cookie 等凭据,不受 API Token 过期影响。
|
|
76
87
|
*
|
|
77
88
|
* 默认复用不超过 24 小时的缓存;缺失、过期或时间异常时重新获取。
|
|
78
89
|
* `forceRefresh: true` 忽略缓存。同一客户端的并发刷新共用首次调用的传输选项;
|
|
@@ -134,6 +145,8 @@ export declare class ZentaoClient {
|
|
|
134
145
|
* 当全局 `persistProfiles` 为真时,会同时把账号、Token、用户信息、服务端配置和
|
|
135
146
|
* 客户端偏好(仅在显式设置过 `timeout` / `insecure` 时)持久化为本地 profile,
|
|
136
147
|
* 并切换为当前 profile,方便下次通过 {@link ZentaoClient.fromProfile} 直接登录态恢复。
|
|
148
|
+
* 重新登录同一账号时保留已有自定义字段,以及未被显式覆盖的客户端偏好。
|
|
149
|
+
* 保存的 `timeout` / `insecure` 与请求一致:全局显式值优先于实例默认值。
|
|
137
150
|
*
|
|
138
151
|
* @param account - 禅道用户账号。
|
|
139
152
|
* @param password - 禅道用户密码(明文,仅在传输层 TLS 内使用)。
|
|
@@ -164,13 +177,16 @@ export declare class ZentaoClient {
|
|
|
164
177
|
/**
|
|
165
178
|
* 根据本地持久化 profile 创建客户端。
|
|
166
179
|
*
|
|
167
|
-
*
|
|
180
|
+
* 默认调用 {@link switchProfile}:若 `profileKey` 存在则刷新其 `lastUsedTime` 并设为当前 profile;
|
|
168
181
|
* 不传 `profileKey` 时使用当前 profile。Profile 中保存的 `timeout` / `insecure` 偏好也会被带回到客户端实例。
|
|
182
|
+
* `activate: false` 时只读存储,不切换账号、不更新时间,支持可读但不可写的存储。
|
|
183
|
+
* 两种模式均不替换全局客户端;后续配置刷新是否写回仍由全局 `persistProfiles` 控制。
|
|
169
184
|
*
|
|
170
185
|
* @param profileKey - 可选的 profile key,格式为 `account@server`;不传时使用当前 profile。
|
|
186
|
+
* @param options - 恢复选项;默认保持切换当前 profile 的行为。
|
|
171
187
|
* @returns 用 profile 还原后的客户端实例。
|
|
172
188
|
* @throws {ZentaoError} `E_NO_PROFILE`(无任何 profile 且未传 key)、`E_PROFILE_NOT_FOUND`(指定 key 不存在)、
|
|
173
|
-
* `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问持久化存储)。
|
|
189
|
+
* `E_PROFILE_STORAGE_INVALID`(存储内容不合法)、`E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问持久化存储)。
|
|
174
190
|
*/
|
|
175
|
-
static fromProfile(profileKey?: string): Promise<ZentaoClient>;
|
|
191
|
+
static fromProfile(profileKey?: string, options?: FromProfileOptions): Promise<ZentaoClient>;
|
|
176
192
|
}
|
package/dist/client/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { isZentaoConfigFetchError, ZentaoError } from '../misc/errors.js';
|
|
|
2
2
|
import { parseZentaoVersion } from '../misc/zentao-version.js';
|
|
3
3
|
import { assertInsecureSupported, fetchWithInsecureTls } from '../misc/environment.js';
|
|
4
4
|
import { getGlobalOptions, setGlobalOptions } from '../misc/global-options.js';
|
|
5
|
-
import {
|
|
5
|
+
import { getProfileOrThrow, saveLoginProfile, switchProfile, updateProfileServerConfig } from '../profiles/index.js';
|
|
6
6
|
import { isRecord, normalizeSiteUrl } from '../utils/index.js';
|
|
7
7
|
const DEFAULT_TIMEOUT = 10000;
|
|
8
8
|
const CONFIG_MAX_AGE = 24 * 60 * 60 * 1000;
|
|
@@ -316,10 +316,19 @@ export class ZentaoClient {
|
|
|
316
316
|
this.insecure = options.insecure;
|
|
317
317
|
}
|
|
318
318
|
async request(path, options = {}) {
|
|
319
|
-
return this.
|
|
319
|
+
return this.fetch(buildUrl(this.baseUrl, path, options.query), options, this.token);
|
|
320
320
|
}
|
|
321
|
-
/**
|
|
322
|
-
|
|
321
|
+
/**
|
|
322
|
+
* 使用完整 URL 发起请求,复用 API 与站点配置的传输层。
|
|
323
|
+
*
|
|
324
|
+
* @param url - 完整请求 URL,包含所需的查询参数。
|
|
325
|
+
* @param options - 单次请求选项;其中 `query` 需由调用方预先拼入 `url`。
|
|
326
|
+
* @param token - 显式注入的 Token;省略时不自动使用实例保存的 Token。
|
|
327
|
+
* @param fetchOptions - 原生 fetch 的缓存与凭据选项。
|
|
328
|
+
* @returns 按 `options.responseType` 解析的响应体,默认优先 JSON,失败后返回文本。
|
|
329
|
+
* @throws {ZentaoError} 传输层失败时抛出,详见 {@link ZentaoClient.request}。
|
|
330
|
+
*/
|
|
331
|
+
async fetch(url, options, token, fetchOptions = {}) {
|
|
323
332
|
const globals = getGlobalOptions();
|
|
324
333
|
const method = options.method ?? 'GET';
|
|
325
334
|
const timeout = options.timeout ?? globals.timeout ?? this.timeout ?? DEFAULT_TIMEOUT;
|
|
@@ -333,7 +342,7 @@ export class ZentaoClient {
|
|
|
333
342
|
method,
|
|
334
343
|
headers,
|
|
335
344
|
redirect: 'manual',
|
|
336
|
-
|
|
345
|
+
...fetchOptions,
|
|
337
346
|
};
|
|
338
347
|
// GET 请求不携带 body,避免浏览器和部分代理拒绝请求。
|
|
339
348
|
if (options.body !== undefined && method !== 'GET') {
|
|
@@ -379,7 +388,9 @@ export class ZentaoClient {
|
|
|
379
388
|
}
|
|
380
389
|
}
|
|
381
390
|
/**
|
|
382
|
-
*
|
|
391
|
+
* 匿名获取禅道站点 `/?mode=getconfig` 配置,只需站点地址,无需登录或 profile。
|
|
392
|
+
*
|
|
393
|
+
* 不发送 API Token,浏览器请求显式省略 Cookie 等凭据,不受 API Token 过期影响。
|
|
383
394
|
*
|
|
384
395
|
* 默认复用不超过 24 小时的缓存;缺失、过期或时间异常时重新获取。
|
|
385
396
|
* `forceRefresh: true` 忽略缓存。同一客户端的并发刷新共用首次调用的传输选项;
|
|
@@ -407,9 +418,9 @@ export class ZentaoClient {
|
|
|
407
418
|
parseZentaoVersion(this.serverConfig.version);
|
|
408
419
|
return this.serverConfig;
|
|
409
420
|
}
|
|
410
|
-
const pending = this.
|
|
421
|
+
const pending = this.fetch(buildUrl(this.siteUrl, '/', { mode: 'getconfig' }), {
|
|
411
422
|
method: 'GET', timeout: options.timeout, insecure: options.insecure, signal: options.signal,
|
|
412
|
-
}, undefined, 'no-store').then(async (config) => {
|
|
423
|
+
}, undefined, { cache: 'no-store', credentials: 'omit' }).then(async (config) => {
|
|
413
424
|
if (!isServerConfig(config))
|
|
414
425
|
throw new ZentaoError('E_INVALID_ZENTAO_CONFIG');
|
|
415
426
|
parseZentaoVersion(config.version);
|
|
@@ -500,6 +511,8 @@ export class ZentaoClient {
|
|
|
500
511
|
* 当全局 `persistProfiles` 为真时,会同时把账号、Token、用户信息、服务端配置和
|
|
501
512
|
* 客户端偏好(仅在显式设置过 `timeout` / `insecure` 时)持久化为本地 profile,
|
|
502
513
|
* 并切换为当前 profile,方便下次通过 {@link ZentaoClient.fromProfile} 直接登录态恢复。
|
|
514
|
+
* 重新登录同一账号时保留已有自定义字段,以及未被显式覆盖的客户端偏好。
|
|
515
|
+
* 保存的 `timeout` / `insecure` 与请求一致:全局显式值优先于实例默认值。
|
|
503
516
|
*
|
|
504
517
|
* @param account - 禅道用户账号。
|
|
505
518
|
* @param password - 禅道用户密码(明文,仅在传输层 TLS 内使用)。
|
|
@@ -525,13 +538,13 @@ export class ZentaoClient {
|
|
|
525
538
|
let profileKey;
|
|
526
539
|
if (globals.persistProfiles) {
|
|
527
540
|
const config = {};
|
|
528
|
-
const timeout =
|
|
529
|
-
const insecure =
|
|
541
|
+
const timeout = globals.timeout ?? this.timeout;
|
|
542
|
+
const insecure = globals.insecure ?? this.insecure;
|
|
530
543
|
if (timeout !== undefined)
|
|
531
544
|
config.timeout = timeout;
|
|
532
545
|
if (insecure !== undefined)
|
|
533
546
|
config.insecure = insecure;
|
|
534
|
-
const profile = await
|
|
547
|
+
const profile = await saveLoginProfile({
|
|
535
548
|
server: this.siteUrl,
|
|
536
549
|
account,
|
|
537
550
|
token: response.token,
|
|
@@ -574,18 +587,19 @@ export class ZentaoClient {
|
|
|
574
587
|
/**
|
|
575
588
|
* 根据本地持久化 profile 创建客户端。
|
|
576
589
|
*
|
|
577
|
-
*
|
|
590
|
+
* 默认调用 {@link switchProfile}:若 `profileKey` 存在则刷新其 `lastUsedTime` 并设为当前 profile;
|
|
578
591
|
* 不传 `profileKey` 时使用当前 profile。Profile 中保存的 `timeout` / `insecure` 偏好也会被带回到客户端实例。
|
|
592
|
+
* `activate: false` 时只读存储,不切换账号、不更新时间,支持可读但不可写的存储。
|
|
593
|
+
* 两种模式均不替换全局客户端;后续配置刷新是否写回仍由全局 `persistProfiles` 控制。
|
|
579
594
|
*
|
|
580
595
|
* @param profileKey - 可选的 profile key,格式为 `account@server`;不传时使用当前 profile。
|
|
596
|
+
* @param options - 恢复选项;默认保持切换当前 profile 的行为。
|
|
581
597
|
* @returns 用 profile 还原后的客户端实例。
|
|
582
598
|
* @throws {ZentaoError} `E_NO_PROFILE`(无任何 profile 且未传 key)、`E_PROFILE_NOT_FOUND`(指定 key 不存在)、
|
|
583
|
-
* `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问持久化存储)。
|
|
599
|
+
* `E_PROFILE_STORAGE_INVALID`(存储内容不合法)、`E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问持久化存储)。
|
|
584
600
|
*/
|
|
585
|
-
static async fromProfile(profileKey) {
|
|
586
|
-
|
|
587
|
-
// 若 key 不存在会抛出 E_PROFILE_NOT_FOUND;不传 key 时由 switchCurrentProfile 处理。
|
|
588
|
-
const activeProfile = await switchProfile(profileKey);
|
|
601
|
+
static async fromProfile(profileKey, options = {}) {
|
|
602
|
+
const activeProfile = options.activate === false ? await getProfileOrThrow(profileKey) : await switchProfile(profileKey);
|
|
589
603
|
const client = new ZentaoClient({
|
|
590
604
|
baseUrl: activeProfile.server,
|
|
591
605
|
token: activeProfile.token,
|
package/dist/misc/errors.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export declare const ERRORS: {
|
|
|
19
19
|
readonly E_INVALID_PROFILE: "Invalid ZenTao profile.";
|
|
20
20
|
readonly E_NO_PROFILE: "No ZenTao profile is configured.";
|
|
21
21
|
readonly E_PROFILE_NOT_FOUND: "ZenTao profile not found: {profileKey}";
|
|
22
|
-
readonly E_PROFILE_STORAGE_INVALID: "ZenTao profile storage
|
|
22
|
+
readonly E_PROFILE_STORAGE_INVALID: "ZenTao profile storage must be a JSON object with a profiles array and an optional currentProfile string.";
|
|
23
23
|
readonly E_PROFILE_STORAGE_UNAVAILABLE: "ZenTao profile storage is unavailable in this runtime.";
|
|
24
24
|
readonly E_INVALID_MODULE: "Unknown module: {module}";
|
|
25
25
|
readonly E_INVALID_ACTION: "Unknown action: {module}-{action}";
|
package/dist/misc/errors.js
CHANGED
|
@@ -19,7 +19,7 @@ export const ERRORS = {
|
|
|
19
19
|
E_INVALID_PROFILE: 'Invalid ZenTao profile.',
|
|
20
20
|
E_NO_PROFILE: 'No ZenTao profile is configured.',
|
|
21
21
|
E_PROFILE_NOT_FOUND: 'ZenTao profile not found: {profileKey}',
|
|
22
|
-
E_PROFILE_STORAGE_INVALID: 'ZenTao profile storage
|
|
22
|
+
E_PROFILE_STORAGE_INVALID: 'ZenTao profile storage must be a JSON object with a profiles array and an optional currentProfile string.',
|
|
23
23
|
E_PROFILE_STORAGE_UNAVAILABLE: 'ZenTao profile storage is unavailable in this runtime.',
|
|
24
24
|
E_INVALID_MODULE: 'Unknown module: {module}',
|
|
25
25
|
E_INVALID_ACTION: 'Unknown action: {module}-{action}',
|
|
@@ -156,6 +156,9 @@ export function validateAction(action) {
|
|
|
156
156
|
if (action.resultType !== undefined && typeof action.resultType !== 'string') {
|
|
157
157
|
throw new ZentaoError('E_INVALID_ACTION_DEFINITION');
|
|
158
158
|
}
|
|
159
|
+
if (action.request !== undefined && typeof action.request !== 'function') {
|
|
160
|
+
throw new ZentaoError('E_INVALID_ACTION_DEFINITION');
|
|
161
|
+
}
|
|
159
162
|
}
|
|
160
163
|
/** 当前运行时注册表中的模块数组(define 侧原地修改,query 侧只读)。 */
|
|
161
164
|
export function getModulesState() {
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
// 间接导入,避免浏览器打包器解析 Node 内置模块。
|
|
3
|
+
function importNodeModule(specifier) {
|
|
4
|
+
return import(specifier);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* 在同一主机的本地文件系统上保护 profile 的 read-modify-write。
|
|
8
|
+
* 仅回收已确认退出的本机进程;不以锁的年龄判断进程是否仍在写入。
|
|
9
|
+
* @internal
|
|
10
|
+
*/
|
|
11
|
+
export async function withProfileFileLock(file, operation, timeoutMs = 5000) {
|
|
12
|
+
const [fs, path, os, crypto] = await Promise.all([
|
|
13
|
+
importNodeModule('node:fs/promises'),
|
|
14
|
+
importNodeModule('node:path'),
|
|
15
|
+
importNodeModule('node:os'),
|
|
16
|
+
importNodeModule('node:crypto'),
|
|
17
|
+
]);
|
|
18
|
+
await fs.mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
19
|
+
const directory = await fs.realpath(path.dirname(file));
|
|
20
|
+
const lock = path.join(directory, `${path.basename(file)}.lock`);
|
|
21
|
+
const ownerName = `${process.pid}-${crypto.randomUUID()}.json`;
|
|
22
|
+
const candidate = `${lock}.${ownerName}`;
|
|
23
|
+
const hostname = os.hostname();
|
|
24
|
+
async function removeOwner(name) {
|
|
25
|
+
try {
|
|
26
|
+
await fs.unlink(path.join(lock, name));
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error.code === 'ENOENT')
|
|
30
|
+
return;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
// 迟到的释放者/回收者不能删除后继持有者的非空目录。
|
|
35
|
+
await fs.rmdir(lock);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code ?? ''))
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function reclaimDeadOwner() {
|
|
43
|
+
try {
|
|
44
|
+
const entries = await fs.readdir(lock, { withFileTypes: true });
|
|
45
|
+
if (entries.length !== 1 || !entries[0].isFile())
|
|
46
|
+
return;
|
|
47
|
+
const name = entries[0].name;
|
|
48
|
+
if (!/^[1-9]\d*-[\da-f]{8}-(?:[\da-f]{4}-){3}[\da-f]{12}\.json$/.test(name))
|
|
49
|
+
return;
|
|
50
|
+
let owner;
|
|
51
|
+
try {
|
|
52
|
+
owner = JSON.parse(await fs.readFile(path.join(lock, name), 'utf8'));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof SyntaxError)
|
|
56
|
+
return;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (!owner || typeof owner.pid !== 'number' || !Number.isSafeInteger(owner.pid)
|
|
60
|
+
|| owner.pid <= 0 || !name.startsWith(`${owner.pid}-`) || owner.hostname !== hostname)
|
|
61
|
+
return;
|
|
62
|
+
try {
|
|
63
|
+
process.kill(owner.pid, 0);
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error.code === 'ESRCH')
|
|
67
|
+
await removeOwner(name);
|
|
68
|
+
// EPERM 或未知错误都不能证明进程已退出。
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT')
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await fs.mkdir(candidate, { mode: 0o700 });
|
|
77
|
+
let acquired = false;
|
|
78
|
+
try {
|
|
79
|
+
await fs.writeFile(path.join(candidate, ownerName), JSON.stringify({ pid: process.pid, hostname }), {
|
|
80
|
+
flag: 'wx', mode: 0o600,
|
|
81
|
+
});
|
|
82
|
+
const started = performance.now();
|
|
83
|
+
while (performance.now() - started < timeoutMs) {
|
|
84
|
+
try {
|
|
85
|
+
// 先准备非空目录再原子 rename,不留下“已有锁但尚无 owner”的窗口。
|
|
86
|
+
await fs.rename(candidate, lock);
|
|
87
|
+
acquired = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const code = error.code;
|
|
92
|
+
if (code === 'EPERM') {
|
|
93
|
+
// Windows 对已有目录可能返回 EPERM;没有目标目录时保留真实权限错误。
|
|
94
|
+
const existing = await fs.stat(lock).catch(statError => {
|
|
95
|
+
if (statError.code === 'ENOENT')
|
|
96
|
+
return undefined;
|
|
97
|
+
throw statError;
|
|
98
|
+
});
|
|
99
|
+
if (!existing?.isDirectory())
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
else if (code !== 'EEXIST' && code !== 'ENOTEMPTY') {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
await reclaimDeadOwner();
|
|
106
|
+
const remaining = timeoutMs - (performance.now() - started);
|
|
107
|
+
if (remaining > 0) {
|
|
108
|
+
await new Promise(resolve => setTimeout(resolve, Math.min(remaining, 25 + Math.random() * 25)));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (!acquired) {
|
|
113
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE', undefined, new Error('Timed out waiting for the profile file lock.'));
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
return await operation();
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
await removeOwner(ownerName);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
// 此候选目录只属于本次调用;固定锁目录绝不能递归删除。
|
|
124
|
+
if (!acquired)
|
|
125
|
+
await fs.rm(candidate, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
}
|
package/dist/profiles/index.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export declare function getProfileKey(profile: Pick<ZentaoProfile, 'account' | '
|
|
|
23
23
|
* 读取过程不会写回存储;存储中无法解析的条目会被静默忽略,不会影响其余 profile。
|
|
24
24
|
*
|
|
25
25
|
* @returns 当前存储中的所有 profile(带 `key` 字段),文件不存在时返回空数组。
|
|
26
|
-
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID`(存储内容不是合法 JSON
|
|
26
|
+
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID`(存储内容不是合法 JSON 或根结构不合法)或
|
|
27
27
|
* `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问存储)。
|
|
28
28
|
*/
|
|
29
29
|
export declare function getAllProfiles(): Promise<ZentaoProfileRecord[]>;
|
|
@@ -35,13 +35,16 @@ export declare function getAllProfiles(): Promise<ZentaoProfileRecord[]>;
|
|
|
35
35
|
* @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID` / `E_PROFILE_STORAGE_UNAVAILABLE`。
|
|
36
36
|
*/
|
|
37
37
|
export declare function getProfile(profileKey?: string): Promise<ZentaoProfileRecord | undefined>;
|
|
38
|
+
/** 只读恢复与账号切换共用相同的 key 解析和错误语义。 @internal */
|
|
39
|
+
export declare function getProfileOrThrow(profileKey?: string): Promise<ZentaoProfileRecord>;
|
|
38
40
|
/**
|
|
39
41
|
* 添加或覆盖一个本地 profile,并把它设置为当前使用的 profile。
|
|
40
42
|
*
|
|
41
43
|
* 行为细节:
|
|
42
44
|
* - 同 key(`account@server`)已存在时会**整体覆盖**而非合并字段。
|
|
43
45
|
* - 写入时会自动补齐 `loginTime` 与 `lastUsedTime`(若调用方未提供则使用当前 ISO 时间)。
|
|
44
|
-
* -
|
|
46
|
+
* - Node.js 通过文件锁保护同主机本地文件系统的 read-modify-write;浏览器在支持 Web Locks 时保护同源上下文,其他环境仅保证实例内串行。
|
|
47
|
+
* - 跨进程或上下文等待锁超过约 5 秒时抛出存储不可用错误,不修改 profile 数据。
|
|
45
48
|
* - 实际写入使用临时文件 + `rename` 的原子方式,并将文件与目录权限收紧到 `0600`/`0700`(Node.js 下)。
|
|
46
49
|
*
|
|
47
50
|
* @param profile - 要写入的 profile,必须至少包含 `server`、`account`、`token`。
|
|
@@ -50,12 +53,14 @@ export declare function getProfile(profileKey?: string): Promise<ZentaoProfileRe
|
|
|
50
53
|
* `E_INVALID_BASE_URL`、`E_PROFILE_STORAGE_INVALID`、`E_PROFILE_STORAGE_UNAVAILABLE`。
|
|
51
54
|
*/
|
|
52
55
|
export declare function addProfile(profile: ZentaoProfile): Promise<ZentaoProfileRecord>;
|
|
56
|
+
/** 登录只更新会话字段和显式偏好,保留同账号的应用数据。 @internal */
|
|
57
|
+
export declare function saveLoginProfile(profile: ZentaoProfile): Promise<ZentaoProfileRecord>;
|
|
53
58
|
/** 只刷新已有 profile 的服务器配置,不切换当前账号或重建已删除的记录。 @internal */
|
|
54
59
|
export declare function updateProfileServerConfig(profileKey: string, serverConfig: ServerConfig, fetchedAt: string): Promise<void>;
|
|
55
60
|
/**
|
|
56
61
|
* 删除指定 profile。
|
|
57
62
|
*
|
|
58
|
-
* 若被删除的是当前 profile
|
|
63
|
+
* 若被删除的是当前 profile,会回退为最近添加且仍保留的 profile;覆盖或切换已有记录不改变添加顺序。若已无任何 profile,
|
|
59
64
|
* 当前 profile 会被清空。操作同样通过进程内串行锁保护。
|
|
60
65
|
*
|
|
61
66
|
* @param profileKey - 要删除的 profile key。
|