zentao-api 0.2.0-beta.1 → 0.2.0-beta.2
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 +170 -36
- package/dist/browser/zentao-api.global.js +2 -1
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +46 -23
- 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 +108 -14
- package/dist/misc/errors.d.ts +5 -0
- package/dist/misc/errors.js +5 -0
- package/dist/modules/registry.d.ts +2 -2
- package/dist/modules/registry.js +23 -10
- package/dist/modules/resolve.js +20 -10
- package/dist/profiles/index.d.ts +14 -0
- package/dist/profiles/index.js +205 -0
- package/dist/types/index.d.ts +61 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +12 -0
- package/dist/version.js +2 -2
- package/package.json +14 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { ZentaoClient } from './client/index.js';
|
|
2
2
|
export { ERRORS, ZentaoError, type ErrorCode } from './misc/errors.js';
|
|
3
3
|
export { getGlobalOptions, setGlobalOptions } from './misc/global-options.js';
|
|
4
|
+
export { ZENTAO_PROFILES_STORAGE_KEY, addProfile, deleteProfile, getAllProfiles, getProfile, getProfileKey, switchProfile, } from './profiles/index.js';
|
|
4
5
|
export { defineModuleActions, defineModules, type DefineModulesOptions, getModule, getModuleAction, } from './modules/registry.js';
|
|
5
6
|
export { request } from './request/index.js';
|
|
6
7
|
export { BUILD, VERSION } from './version.js';
|
|
7
|
-
export type { ApiListResponse, ApiResponse, ClientRequestOptions, GlobalOptions, HttpMethod, ListPagerInfo, LoginResponse, ModuleAction, ModuleActionMethod, ModuleActionName, ModuleActionPagerGetterMap, ModuleActionParam, ModuleActionParamOption, ModuleActionRequestBody, ModuleActionResponse, ModuleActionResultRender, ModuleActionResultRenderType, ModuleActionResultType, ModuleActionType, ModuleDefinition, ModuleName, Pager, RequestOptions, ResolvedModuleCommand, ResponseData, ServerConfig, ZentaoClientOptions, } from './types/index.js';
|
|
8
|
+
export type { ApiListResponse, ApiResponse, ClientRequestOptions, GlobalOptions, HttpMethod, ListPagerInfo, LoginResponse, ModuleAction, ModuleActionMethod, ModuleActionName, ModuleActionPagerGetterMap, ModuleActionParam, ModuleActionParamOption, ModuleActionRequestBody, ModuleActionResponse, ModuleActionResultRender, ModuleActionResultRenderType, ModuleActionResultType, ModuleActionType, ModuleDefinition, ModuleName, Pager, RequestOptions, ResolvedModuleCommand, ResponseData, ServerConfig, ZentaoProfile, ZentaoProfileConfig, ZentaoProfileRecord, ZentaoProfilesStore, ZentaoClientOptions, } from './types/index.js';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { ZentaoClient } from './client/index.js';
|
|
2
2
|
export { ERRORS, ZentaoError } from './misc/errors.js';
|
|
3
3
|
export { getGlobalOptions, setGlobalOptions } from './misc/global-options.js';
|
|
4
|
+
export { ZENTAO_PROFILES_STORAGE_KEY, addProfile, deleteProfile, getAllProfiles, getProfile, getProfileKey, switchProfile, } from './profiles/index.js';
|
|
4
5
|
export { defineModuleActions, defineModules, getModule, getModuleAction, } from './modules/registry.js';
|
|
5
6
|
export { request } from './request/index.js';
|
|
6
7
|
export { BUILD, VERSION } from './version.js';
|
|
@@ -2,5 +2,7 @@
|
|
|
2
2
|
export declare function isNodeRuntime(): boolean;
|
|
3
3
|
/** 浏览器无法跳过 TLS 校验,因此在发起请求前提前失败。 */
|
|
4
4
|
export declare function assertInsecureSupported(enabled: boolean | undefined): void;
|
|
5
|
-
/**
|
|
5
|
+
/** 发起 fetch 请求;Node.js 下的 `insecure` 只作用于当前 HTTPS 请求。 */
|
|
6
|
+
export declare function fetchWithInsecureTls(enabled: boolean | undefined, url: string, init: RequestInit): Promise<Response>;
|
|
7
|
+
/** 保留给内部测试和兼容调用:校验 TLS 选项,但不再改写进程级环境变量。 */
|
|
6
8
|
export declare function withInsecureTls<T>(enabled: boolean | undefined, fn: () => Promise<T>): Promise<T>;
|
package/dist/misc/environment.js
CHANGED
|
@@ -3,28 +3,122 @@ import { ZentaoError } from './errors.js';
|
|
|
3
3
|
export function isNodeRuntime() {
|
|
4
4
|
return typeof process !== 'undefined' && Boolean(process.versions?.node);
|
|
5
5
|
}
|
|
6
|
+
async function importNodeModule(specifier) {
|
|
7
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
8
|
+
return dynamicImport(specifier);
|
|
9
|
+
}
|
|
10
|
+
function toNodeRequestHeaders(headers) {
|
|
11
|
+
const result = {};
|
|
12
|
+
new Headers(headers).forEach((value, key) => {
|
|
13
|
+
result[key] = value;
|
|
14
|
+
});
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function toResponseHeaders(headers) {
|
|
18
|
+
const result = new Headers();
|
|
19
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
20
|
+
if (value === undefined)
|
|
21
|
+
continue;
|
|
22
|
+
result.set(key, Array.isArray(value) ? value.join(', ') : String(value));
|
|
23
|
+
}
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
async function toNodeBody(body) {
|
|
27
|
+
if (body === undefined || body === null)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (typeof body === 'string')
|
|
30
|
+
return body;
|
|
31
|
+
if (body instanceof Uint8Array)
|
|
32
|
+
return body;
|
|
33
|
+
if (body instanceof ArrayBuffer)
|
|
34
|
+
return new Uint8Array(body);
|
|
35
|
+
if (ArrayBuffer.isView(body)) {
|
|
36
|
+
return new Uint8Array(body.buffer, body.byteOffset, body.byteLength);
|
|
37
|
+
}
|
|
38
|
+
if (body instanceof Blob) {
|
|
39
|
+
return new Uint8Array(await body.arrayBuffer());
|
|
40
|
+
}
|
|
41
|
+
return String(body);
|
|
42
|
+
}
|
|
43
|
+
function abortError() {
|
|
44
|
+
return new DOMException('The operation was aborted.', 'AbortError');
|
|
45
|
+
}
|
|
46
|
+
function concatenateChunks(chunks) {
|
|
47
|
+
const totalLength = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
48
|
+
const result = new Uint8Array(totalLength);
|
|
49
|
+
let offset = 0;
|
|
50
|
+
for (const chunk of chunks) {
|
|
51
|
+
result.set(chunk, offset);
|
|
52
|
+
offset += chunk.byteLength;
|
|
53
|
+
}
|
|
54
|
+
return result.buffer;
|
|
55
|
+
}
|
|
56
|
+
async function nodeFetchWithTlsOptions(url, init, rejectUnauthorized) {
|
|
57
|
+
const parsed = new URL(url);
|
|
58
|
+
const transport = parsed.protocol === 'https:'
|
|
59
|
+
? await importNodeModule('node:https')
|
|
60
|
+
: await importNodeModule('node:http');
|
|
61
|
+
const body = await toNodeBody(init.body);
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
if (init.signal?.aborted) {
|
|
64
|
+
reject(abortError());
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const request = transport.request(parsed, {
|
|
68
|
+
method: init.method ?? 'GET',
|
|
69
|
+
headers: toNodeRequestHeaders(init.headers),
|
|
70
|
+
rejectUnauthorized,
|
|
71
|
+
}, (response) => {
|
|
72
|
+
const chunks = [];
|
|
73
|
+
response.on('data', (chunk) => {
|
|
74
|
+
chunks.push(typeof chunk === 'string' ? new TextEncoder().encode(chunk) : chunk);
|
|
75
|
+
});
|
|
76
|
+
response.on('end', () => {
|
|
77
|
+
cleanup();
|
|
78
|
+
const responseBody = chunks.length > 0 ? concatenateChunks(chunks) : undefined;
|
|
79
|
+
const fetchResponse = new Response(responseBody, {
|
|
80
|
+
status: response.statusCode ?? 200,
|
|
81
|
+
statusText: response.statusMessage ?? '',
|
|
82
|
+
headers: toResponseHeaders(response.headers),
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperty(fetchResponse, 'url', { value: url });
|
|
85
|
+
resolve(fetchResponse);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
const cleanup = () => {
|
|
89
|
+
init.signal?.removeEventListener('abort', abortHandler);
|
|
90
|
+
};
|
|
91
|
+
const abortHandler = () => {
|
|
92
|
+
cleanup();
|
|
93
|
+
request.destroy(abortError());
|
|
94
|
+
};
|
|
95
|
+
request.on('error', (error) => {
|
|
96
|
+
cleanup();
|
|
97
|
+
reject(error);
|
|
98
|
+
});
|
|
99
|
+
init.signal?.addEventListener('abort', abortHandler, { once: true });
|
|
100
|
+
if (body !== undefined)
|
|
101
|
+
request.write(body);
|
|
102
|
+
request.end();
|
|
103
|
+
});
|
|
104
|
+
}
|
|
6
105
|
/** 浏览器无法跳过 TLS 校验,因此在发起请求前提前失败。 */
|
|
7
106
|
export function assertInsecureSupported(enabled) {
|
|
8
107
|
if (enabled && !isNodeRuntime()) {
|
|
9
108
|
throw new ZentaoError('E_INSECURE_BROWSER');
|
|
10
109
|
}
|
|
11
110
|
}
|
|
12
|
-
/**
|
|
111
|
+
/** 发起 fetch 请求;Node.js 下的 `insecure` 只作用于当前 HTTPS 请求。 */
|
|
112
|
+
export async function fetchWithInsecureTls(enabled, url, init) {
|
|
113
|
+
if (!enabled)
|
|
114
|
+
return fetch(url, init);
|
|
115
|
+
assertInsecureSupported(enabled);
|
|
116
|
+
return nodeFetchWithTlsOptions(url, init, false);
|
|
117
|
+
}
|
|
118
|
+
/** 保留给内部测试和兼容调用:校验 TLS 选项,但不再改写进程级环境变量。 */
|
|
13
119
|
export async function withInsecureTls(enabled, fn) {
|
|
14
120
|
if (!enabled)
|
|
15
121
|
return fn();
|
|
16
122
|
assertInsecureSupported(enabled);
|
|
17
|
-
|
|
18
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
|
19
|
-
try {
|
|
20
|
-
return await fn();
|
|
21
|
-
}
|
|
22
|
-
finally {
|
|
23
|
-
if (previous === undefined) {
|
|
24
|
-
delete process.env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
25
|
-
}
|
|
26
|
-
else {
|
|
27
|
-
process.env.NODE_TLS_REJECT_UNAUTHORIZED = previous;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
123
|
+
return fn();
|
|
30
124
|
}
|
package/dist/misc/errors.d.ts
CHANGED
|
@@ -6,6 +6,11 @@ export declare const ERRORS: {
|
|
|
6
6
|
readonly E_TIMEOUT: "Request timed out.";
|
|
7
7
|
readonly E_INSECURE_BROWSER: "The insecure option is only supported in Node.js runtimes.";
|
|
8
8
|
readonly E_LOGIN_FAILED: "ZenTao login failed.";
|
|
9
|
+
readonly E_INVALID_PROFILE: "Invalid ZenTao profile.";
|
|
10
|
+
readonly E_NO_PROFILE: "No ZenTao profile is configured.";
|
|
11
|
+
readonly E_PROFILE_NOT_FOUND: "ZenTao profile not found: {profileKey}";
|
|
12
|
+
readonly E_PROFILE_STORAGE_INVALID: "ZenTao profile storage is not valid JSON.";
|
|
13
|
+
readonly E_PROFILE_STORAGE_UNAVAILABLE: "ZenTao profile storage is unavailable in this runtime.";
|
|
9
14
|
readonly E_INVALID_MODULE: "Unknown module: {module}";
|
|
10
15
|
readonly E_INVALID_ACTION: "Unknown action: {module}-{action}";
|
|
11
16
|
readonly E_INVALID_MODULE_DEFINITION: "Invalid module definition.";
|
package/dist/misc/errors.js
CHANGED
|
@@ -6,6 +6,11 @@ export const ERRORS = {
|
|
|
6
6
|
E_TIMEOUT: 'Request timed out.',
|
|
7
7
|
E_INSECURE_BROWSER: 'The insecure option is only supported in Node.js runtimes.',
|
|
8
8
|
E_LOGIN_FAILED: 'ZenTao login failed.',
|
|
9
|
+
E_INVALID_PROFILE: 'Invalid ZenTao profile.',
|
|
10
|
+
E_NO_PROFILE: 'No ZenTao profile is configured.',
|
|
11
|
+
E_PROFILE_NOT_FOUND: 'ZenTao profile not found: {profileKey}',
|
|
12
|
+
E_PROFILE_STORAGE_INVALID: 'ZenTao profile storage is not valid JSON.',
|
|
13
|
+
E_PROFILE_STORAGE_UNAVAILABLE: 'ZenTao profile storage is unavailable in this runtime.',
|
|
9
14
|
E_INVALID_MODULE: 'Unknown module: {module}',
|
|
10
15
|
E_INVALID_ACTION: 'Unknown action: {module}-{action}',
|
|
11
16
|
E_INVALID_MODULE_DEFINITION: 'Invalid module definition.',
|
|
@@ -4,9 +4,9 @@ export { BUILTIN_MODULES };
|
|
|
4
4
|
export declare const MODULES: ModuleDefinition[];
|
|
5
5
|
export interface DefineModulesOptions {
|
|
6
6
|
/** 同名模块是否整体替换;默认合并模块定义和动作。 */
|
|
7
|
-
|
|
7
|
+
replace?: boolean;
|
|
8
8
|
}
|
|
9
|
-
/** 定义或扩展模块;同名模块默认合并动作,`
|
|
9
|
+
/** 定义或扩展模块;同名模块默认合并动作,`replace` 为真时整体替换,未知模块追加。 */
|
|
10
10
|
export declare function defineModules(input: ModuleDefinition | ModuleDefinition[], options?: DefineModulesOptions): void;
|
|
11
11
|
/** 为已存在模块定义或覆盖动作;同名动作替换,未知动作追加。 */
|
|
12
12
|
export declare function defineModuleActions(moduleName: string, input: ModuleAction | ModuleAction[]): void;
|
package/dist/modules/registry.js
CHANGED
|
@@ -6,12 +6,25 @@ export const MODULES = BUILTIN_MODULES;
|
|
|
6
6
|
// 运行时注册表使用内置定义的浅克隆,避免用户扩展污染生成文件导出的常量。
|
|
7
7
|
let modules = cloneModules(BUILTIN_MODULES);
|
|
8
8
|
let moduleMap = buildModuleMap(modules);
|
|
9
|
+
function cloneValue(value) {
|
|
10
|
+
if (Array.isArray(value)) {
|
|
11
|
+
return value.map(cloneValue);
|
|
12
|
+
}
|
|
13
|
+
if (value && typeof value === 'object') {
|
|
14
|
+
const result = {};
|
|
15
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
16
|
+
result[key] = cloneValue(nestedValue);
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
9
22
|
function cloneActions(source) {
|
|
10
|
-
return source.map((action) => (
|
|
23
|
+
return source.map((action) => cloneValue(action));
|
|
11
24
|
}
|
|
12
25
|
function cloneModules(source) {
|
|
13
26
|
return source.map((module) => ({
|
|
14
|
-
...module,
|
|
27
|
+
...cloneValue(module),
|
|
15
28
|
actions: cloneActions(module.actions),
|
|
16
29
|
}));
|
|
17
30
|
}
|
|
@@ -23,7 +36,7 @@ function mergeActions(base, extension) {
|
|
|
23
36
|
const next = cloneActions(base);
|
|
24
37
|
for (const action of extension) {
|
|
25
38
|
const index = findActionIndex(next, String(action.name));
|
|
26
|
-
const clone =
|
|
39
|
+
const clone = cloneValue(action);
|
|
27
40
|
if (index >= 0) {
|
|
28
41
|
next[index] = clone;
|
|
29
42
|
}
|
|
@@ -56,15 +69,15 @@ function validateAction(action) {
|
|
|
56
69
|
throw new ZentaoError('E_INVALID_ACTION_DEFINITION');
|
|
57
70
|
}
|
|
58
71
|
}
|
|
59
|
-
/** 定义或扩展模块;同名模块默认合并动作,`
|
|
72
|
+
/** 定义或扩展模块;同名模块默认合并动作,`replace` 为真时整体替换,未知模块追加。 */
|
|
60
73
|
export function defineModules(input, options = {}) {
|
|
61
74
|
for (const module of asArray(input)) {
|
|
62
75
|
validateModule(module);
|
|
63
76
|
const key = module.name.toLowerCase();
|
|
64
77
|
const index = modules.findIndex((item) => item.name.toLowerCase() === key);
|
|
65
|
-
const next = { ...module, actions: cloneActions(module.actions) };
|
|
78
|
+
const next = { ...cloneValue(module), actions: cloneActions(module.actions) };
|
|
66
79
|
if (index >= 0) {
|
|
67
|
-
modules[index] = options.
|
|
80
|
+
modules[index] = options.replace ? next : mergeModule(modules[index], module);
|
|
68
81
|
}
|
|
69
82
|
else {
|
|
70
83
|
modules.push(next);
|
|
@@ -81,7 +94,7 @@ export function defineModuleActions(moduleName, input) {
|
|
|
81
94
|
for (const action of asArray(input)) {
|
|
82
95
|
validateAction(action);
|
|
83
96
|
const index = findActionIndex(module.actions, String(action.name));
|
|
84
|
-
const next =
|
|
97
|
+
const next = cloneValue(action);
|
|
85
98
|
// 同名动作替换,未知动作追加;不做深度合并,避免 schema/数组字段出现隐式规则。
|
|
86
99
|
if (index >= 0) {
|
|
87
100
|
module.actions[index] = next;
|
|
@@ -97,7 +110,7 @@ export function getModule(moduleName) {
|
|
|
97
110
|
if (!module) {
|
|
98
111
|
throw new ZentaoError('E_INVALID_MODULE', { module: moduleName });
|
|
99
112
|
}
|
|
100
|
-
return module;
|
|
113
|
+
return cloneModules([module])[0];
|
|
101
114
|
}
|
|
102
115
|
/** 获取指定模块动作;`ls` 会作为 `list` 的别名处理。 */
|
|
103
116
|
export function getModuleAction(moduleName, actionName) {
|
|
@@ -105,12 +118,12 @@ export function getModuleAction(moduleName, actionName) {
|
|
|
105
118
|
const normalized = actionName === 'ls' ? 'list' : actionName;
|
|
106
119
|
const direct = module.actions.find((action) => String(action.name).toLowerCase() === normalized.toLowerCase());
|
|
107
120
|
if (direct)
|
|
108
|
-
return direct;
|
|
121
|
+
return cloneValue(direct);
|
|
109
122
|
const crud = new Set(['list', 'get', 'create', 'update', 'delete']);
|
|
110
123
|
if (!crud.has(normalized)) {
|
|
111
124
|
const custom = module.actions.find((action) => action.type === 'action' && String(action.name).toLowerCase() === normalized.toLowerCase());
|
|
112
125
|
if (custom)
|
|
113
|
-
return custom;
|
|
126
|
+
return cloneValue(custom);
|
|
114
127
|
}
|
|
115
128
|
throw new ZentaoError('E_INVALID_ACTION', { module: moduleName, action: actionName });
|
|
116
129
|
}
|
package/dist/modules/resolve.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
-
import { getNestedValue } from '../utils/index.js';
|
|
2
|
+
import { getNestedValue, isRecord } from '../utils/index.js';
|
|
3
3
|
import { getModuleAction } from './registry.js';
|
|
4
4
|
const SCOPE_MAP = {
|
|
5
5
|
product: 'products',
|
|
@@ -45,9 +45,6 @@ function parseData(value) {
|
|
|
45
45
|
}
|
|
46
46
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : undefined;
|
|
47
47
|
}
|
|
48
|
-
function isPlainObject(value) {
|
|
49
|
-
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
50
|
-
}
|
|
51
48
|
/** 按 OpenAPI schema 的基础类型对参数做轻量转换。 */
|
|
52
49
|
function coerceValue(value, type) {
|
|
53
50
|
if (value === undefined)
|
|
@@ -57,11 +54,24 @@ function coerceValue(value, type) {
|
|
|
57
54
|
return Number.isNaN(numberValue) ? value : numberValue;
|
|
58
55
|
}
|
|
59
56
|
if (type === 'boolean') {
|
|
60
|
-
if (value === '
|
|
61
|
-
return
|
|
62
|
-
if (value === '
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
if (typeof value === 'boolean')
|
|
58
|
+
return value;
|
|
59
|
+
if (typeof value === 'number') {
|
|
60
|
+
if (value === 1)
|
|
61
|
+
return true;
|
|
62
|
+
if (value === 0)
|
|
63
|
+
return false;
|
|
64
|
+
return value;
|
|
65
|
+
}
|
|
66
|
+
if (typeof value === 'string') {
|
|
67
|
+
const normalized = value.trim().toLowerCase();
|
|
68
|
+
if (['true', '1', 'yes', 'on'].includes(normalized))
|
|
69
|
+
return true;
|
|
70
|
+
if (['false', '0', 'no', 'off'].includes(normalized))
|
|
71
|
+
return false;
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
65
75
|
}
|
|
66
76
|
return value;
|
|
67
77
|
}
|
|
@@ -140,7 +150,7 @@ export function resolveModuleCommand(module, actionName, params = {}) {
|
|
|
140
150
|
if (typeof value === 'string') {
|
|
141
151
|
value = value.split(',');
|
|
142
152
|
}
|
|
143
|
-
else if (!hasDataValue || !
|
|
153
|
+
else if (!hasDataValue || !isRecord(value)) {
|
|
144
154
|
value = [value];
|
|
145
155
|
}
|
|
146
156
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ZentaoProfile, ZentaoProfileRecord } from '../types/index.js';
|
|
2
|
+
export declare const ZENTAO_PROFILES_STORAGE_KEY = "ZENTAO_PROFILES";
|
|
3
|
+
/** 根据 profile 的账号和禅道地址生成稳定 key。 */
|
|
4
|
+
export declare function getProfileKey(profile: Pick<ZentaoProfile, 'account' | 'server'>): string;
|
|
5
|
+
/** 列出所有保存的本地 profile。 */
|
|
6
|
+
export declare function getAllProfiles(): Promise<ZentaoProfileRecord[]>;
|
|
7
|
+
/** 获取指定 profile;不传 key 时返回上次使用的 profile。 */
|
|
8
|
+
export declare function getProfile(profileKey?: string): Promise<ZentaoProfileRecord | undefined>;
|
|
9
|
+
/** 添加或覆盖一个本地 profile,并把它设置为当前使用的 profile。 */
|
|
10
|
+
export declare function addProfile(profile: ZentaoProfile): Promise<ZentaoProfileRecord>;
|
|
11
|
+
/** 删除指定 profile;返回是否实际删除了记录。 */
|
|
12
|
+
export declare function deleteProfile(profileKey: string): Promise<boolean>;
|
|
13
|
+
/** 切换当前使用的 profile,并刷新最后使用时间;不传 key 时使用当前 profile。 */
|
|
14
|
+
export declare function switchProfile(profileKey?: string): Promise<ZentaoProfileRecord>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { ZentaoError } from '../misc/errors.js';
|
|
2
|
+
import { isNodeRuntime } from '../misc/environment.js';
|
|
3
|
+
import { normalizeSiteUrl } from '../utils/index.js';
|
|
4
|
+
export const ZENTAO_PROFILES_STORAGE_KEY = 'ZENTAO_PROFILES';
|
|
5
|
+
const PROFILE_FILE_PARTS = ['.config', 'zentao', 'zentao.json'];
|
|
6
|
+
function isRecord(value) {
|
|
7
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
8
|
+
}
|
|
9
|
+
function cloneJson(value) {
|
|
10
|
+
if (value === undefined)
|
|
11
|
+
return value;
|
|
12
|
+
return JSON.parse(JSON.stringify(value));
|
|
13
|
+
}
|
|
14
|
+
function nowString() {
|
|
15
|
+
return new Date().toISOString();
|
|
16
|
+
}
|
|
17
|
+
async function importNodeModule(specifier) {
|
|
18
|
+
const dynamicImport = new Function('specifier', 'return import(specifier)');
|
|
19
|
+
return dynamicImport(specifier);
|
|
20
|
+
}
|
|
21
|
+
function getBrowserStorage() {
|
|
22
|
+
try {
|
|
23
|
+
return globalThis.localStorage;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
async function getProfileFilePath() {
|
|
30
|
+
const path = await importNodeModule('node:path');
|
|
31
|
+
const home = process.env.HOME
|
|
32
|
+
?? process.env.USERPROFILE
|
|
33
|
+
?? (await importNodeModule('node:os')).homedir();
|
|
34
|
+
if (!home) {
|
|
35
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
36
|
+
}
|
|
37
|
+
return path.join(home, ...PROFILE_FILE_PARTS);
|
|
38
|
+
}
|
|
39
|
+
function profileKeyFromParts(account, server) {
|
|
40
|
+
const normalizedAccount = account.trim();
|
|
41
|
+
const normalizedServer = normalizeSiteUrl(server);
|
|
42
|
+
if (!normalizedAccount)
|
|
43
|
+
throw new ZentaoError('E_INVALID_PROFILE');
|
|
44
|
+
return `${normalizedAccount}@${normalizedServer}`;
|
|
45
|
+
}
|
|
46
|
+
function normalizeProfile(profile) {
|
|
47
|
+
if (!isRecord(profile) || typeof profile.server !== 'string' || typeof profile.account !== 'string' || typeof profile.token !== 'string') {
|
|
48
|
+
throw new ZentaoError('E_INVALID_PROFILE');
|
|
49
|
+
}
|
|
50
|
+
const token = profile.token.trim();
|
|
51
|
+
if (!token)
|
|
52
|
+
throw new ZentaoError('E_INVALID_PROFILE');
|
|
53
|
+
const copy = cloneJson(profile);
|
|
54
|
+
delete copy.key;
|
|
55
|
+
return {
|
|
56
|
+
...copy,
|
|
57
|
+
server: normalizeSiteUrl(profile.server),
|
|
58
|
+
account: profile.account.trim(),
|
|
59
|
+
token,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function normalizeStore(raw) {
|
|
63
|
+
if (!isRecord(raw))
|
|
64
|
+
return { profiles: [] };
|
|
65
|
+
const profiles = Array.isArray(raw.profiles)
|
|
66
|
+
? raw.profiles.flatMap((profile) => {
|
|
67
|
+
try {
|
|
68
|
+
return [normalizeProfile(profile)];
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
: [];
|
|
75
|
+
const currentProfile = typeof raw.currentProfile === 'string' ? raw.currentProfile : undefined;
|
|
76
|
+
return currentProfile ? { currentProfile, profiles } : { profiles };
|
|
77
|
+
}
|
|
78
|
+
function parseStore(text) {
|
|
79
|
+
try {
|
|
80
|
+
return normalizeStore(JSON.parse(text));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new ZentaoError('E_PROFILE_STORAGE_INVALID', undefined, error);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function readStore() {
|
|
87
|
+
if (isNodeRuntime()) {
|
|
88
|
+
const fs = await importNodeModule('node:fs/promises');
|
|
89
|
+
const file = await getProfileFilePath();
|
|
90
|
+
try {
|
|
91
|
+
return parseStore(await fs.readFile(file, 'utf8'));
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error.code === 'ENOENT') {
|
|
95
|
+
return { profiles: [] };
|
|
96
|
+
}
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const storage = getBrowserStorage();
|
|
101
|
+
if (!storage) {
|
|
102
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
103
|
+
}
|
|
104
|
+
const text = storage.getItem(ZENTAO_PROFILES_STORAGE_KEY);
|
|
105
|
+
return text ? parseStore(text) : { profiles: [] };
|
|
106
|
+
}
|
|
107
|
+
async function writeStore(store) {
|
|
108
|
+
const normalizedStore = normalizeStore(store);
|
|
109
|
+
const text = `${JSON.stringify(normalizedStore, null, 2)}\n`;
|
|
110
|
+
if (isNodeRuntime()) {
|
|
111
|
+
const fs = await importNodeModule('node:fs/promises');
|
|
112
|
+
const path = await importNodeModule('node:path');
|
|
113
|
+
const file = await getProfileFilePath();
|
|
114
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
115
|
+
await fs.writeFile(file, text, 'utf8');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const storage = getBrowserStorage();
|
|
119
|
+
if (!storage) {
|
|
120
|
+
throw new ZentaoError('E_PROFILE_STORAGE_UNAVAILABLE');
|
|
121
|
+
}
|
|
122
|
+
storage.setItem(ZENTAO_PROFILES_STORAGE_KEY, text);
|
|
123
|
+
}
|
|
124
|
+
function toRecord(profile) {
|
|
125
|
+
const normalized = normalizeProfile(profile);
|
|
126
|
+
return {
|
|
127
|
+
...cloneJson(normalized),
|
|
128
|
+
key: getProfileKey(normalized),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function findProfile(store, profileKey) {
|
|
132
|
+
return store.profiles.find((profile) => getProfileKey(profile) === profileKey);
|
|
133
|
+
}
|
|
134
|
+
function setFallbackCurrentProfile(store) {
|
|
135
|
+
if (!store.currentProfile || !findProfile(store, store.currentProfile)) {
|
|
136
|
+
const fallback = store.profiles.at(-1);
|
|
137
|
+
store.currentProfile = fallback ? getProfileKey(fallback) : undefined;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** 根据 profile 的账号和禅道地址生成稳定 key。 */
|
|
141
|
+
export function getProfileKey(profile) {
|
|
142
|
+
return profileKeyFromParts(profile.account, profile.server);
|
|
143
|
+
}
|
|
144
|
+
/** 列出所有保存的本地 profile。 */
|
|
145
|
+
export async function getAllProfiles() {
|
|
146
|
+
const store = await readStore();
|
|
147
|
+
return store.profiles.map(toRecord);
|
|
148
|
+
}
|
|
149
|
+
/** 获取指定 profile;不传 key 时返回上次使用的 profile。 */
|
|
150
|
+
export async function getProfile(profileKey) {
|
|
151
|
+
const store = await readStore();
|
|
152
|
+
const key = profileKey ?? store.currentProfile;
|
|
153
|
+
if (!key)
|
|
154
|
+
return undefined;
|
|
155
|
+
const profile = findProfile(store, key);
|
|
156
|
+
return profile ? toRecord(profile) : undefined;
|
|
157
|
+
}
|
|
158
|
+
/** 添加或覆盖一个本地 profile,并把它设置为当前使用的 profile。 */
|
|
159
|
+
export async function addProfile(profile) {
|
|
160
|
+
const store = await readStore();
|
|
161
|
+
const timestamp = nowString();
|
|
162
|
+
const normalized = normalizeProfile({
|
|
163
|
+
...profile,
|
|
164
|
+
loginTime: profile.loginTime ?? timestamp,
|
|
165
|
+
lastUsedTime: profile.lastUsedTime ?? timestamp,
|
|
166
|
+
});
|
|
167
|
+
const profileKey = getProfileKey(normalized);
|
|
168
|
+
const index = store.profiles.findIndex((item) => getProfileKey(item) === profileKey);
|
|
169
|
+
if (index >= 0) {
|
|
170
|
+
store.profiles[index] = normalized;
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
store.profiles.push(normalized);
|
|
174
|
+
}
|
|
175
|
+
store.currentProfile = profileKey;
|
|
176
|
+
await writeStore(store);
|
|
177
|
+
return toRecord(normalized);
|
|
178
|
+
}
|
|
179
|
+
/** 删除指定 profile;返回是否实际删除了记录。 */
|
|
180
|
+
export async function deleteProfile(profileKey) {
|
|
181
|
+
const store = await readStore();
|
|
182
|
+
const nextProfiles = store.profiles.filter((profile) => getProfileKey(profile) !== profileKey);
|
|
183
|
+
if (nextProfiles.length === store.profiles.length)
|
|
184
|
+
return false;
|
|
185
|
+
store.profiles = nextProfiles;
|
|
186
|
+
setFallbackCurrentProfile(store);
|
|
187
|
+
await writeStore(store);
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
/** 切换当前使用的 profile,并刷新最后使用时间;不传 key 时使用当前 profile。 */
|
|
191
|
+
export async function switchProfile(profileKey) {
|
|
192
|
+
const store = await readStore();
|
|
193
|
+
const key = profileKey ?? store.currentProfile;
|
|
194
|
+
if (!key) {
|
|
195
|
+
throw new ZentaoError('E_NO_PROFILE');
|
|
196
|
+
}
|
|
197
|
+
const profile = findProfile(store, key);
|
|
198
|
+
if (!profile) {
|
|
199
|
+
throw new ZentaoError('E_PROFILE_NOT_FOUND', { profileKey: key });
|
|
200
|
+
}
|
|
201
|
+
profile.lastUsedTime = nowString();
|
|
202
|
+
store.currentProfile = key;
|
|
203
|
+
await writeStore(store);
|
|
204
|
+
return toRecord(profile);
|
|
205
|
+
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface GlobalOptions {
|
|
|
22
22
|
timeout?: number;
|
|
23
23
|
/** 默认 TLS 跳过证书验证选项;仅 Node.js 运行时支持。 */
|
|
24
24
|
insecure?: boolean;
|
|
25
|
+
/** 是否在登录成功后把账号、Token 和配置持久化为本地 profile。 */
|
|
26
|
+
persistProfiles?: boolean;
|
|
25
27
|
}
|
|
26
28
|
/** SDK 支持的 HTTP 方法。 */
|
|
27
29
|
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
@@ -98,6 +100,10 @@ export interface ApiListResponse extends ApiResponse {
|
|
|
98
100
|
export interface LoginResponse extends ApiResponse {
|
|
99
101
|
/** 登录成功后返回的 API Token。 */
|
|
100
102
|
token?: string;
|
|
103
|
+
/** 部分禅道环境会随登录响应返回用户信息。 */
|
|
104
|
+
user?: Record<string, unknown>;
|
|
105
|
+
/** 部分禅道环境会随登录响应返回服务端配置。 */
|
|
106
|
+
serverConfig?: ServerConfig;
|
|
101
107
|
}
|
|
102
108
|
/** 禅道 `?mode=getconfig` 返回的服务端配置。 */
|
|
103
109
|
export interface ServerConfig {
|
|
@@ -111,6 +117,61 @@ export interface ServerConfig {
|
|
|
111
117
|
viewVar: string;
|
|
112
118
|
sessionVar: string;
|
|
113
119
|
}
|
|
120
|
+
/** 保存到本地 profile 中的客户端偏好配置。 */
|
|
121
|
+
export interface ZentaoProfileConfig {
|
|
122
|
+
/** 默认输出格式,供 CLI 等上层应用复用。 */
|
|
123
|
+
defaultOutputFormat?: 'markdown' | 'json' | 'raw';
|
|
124
|
+
/** 界面语言。 */
|
|
125
|
+
lang?: string;
|
|
126
|
+
/** 默认分页大小。 */
|
|
127
|
+
defaultRecPerPage?: number;
|
|
128
|
+
/** 是否跳过 TLS 证书验证;仅 Node.js 运行时支持。 */
|
|
129
|
+
insecure?: boolean;
|
|
130
|
+
/** 是否将对象属性中的 HTML 转换为 Markdown。 */
|
|
131
|
+
htmlToMarkdown?: boolean;
|
|
132
|
+
/** 请求超时时间,单位毫秒。 */
|
|
133
|
+
timeout?: number;
|
|
134
|
+
/** 是否在批量操作出错时停止执行后续操作。 */
|
|
135
|
+
batchFailFast?: boolean;
|
|
136
|
+
/** JSON 格式化时是否添加缩进。 */
|
|
137
|
+
jsonPretty?: boolean;
|
|
138
|
+
/** 模块级分页偏好。 */
|
|
139
|
+
pagers?: Record<string, number>;
|
|
140
|
+
/** 允许上层应用保存自定义配置。 */
|
|
141
|
+
[key: string]: unknown;
|
|
142
|
+
}
|
|
143
|
+
/** 本地持久化的禅道账号 profile。 */
|
|
144
|
+
export interface ZentaoProfile {
|
|
145
|
+
/** 禅道站点根地址,不包含 `/api.php/v2`。 */
|
|
146
|
+
server: string;
|
|
147
|
+
/** 用户账号。 */
|
|
148
|
+
account: string;
|
|
149
|
+
/** 禅道 API Token。 */
|
|
150
|
+
token: string;
|
|
151
|
+
/** 登录验证通过后得到的用户信息。 */
|
|
152
|
+
user?: Record<string, unknown>;
|
|
153
|
+
/** 登录时间。 */
|
|
154
|
+
loginTime?: string;
|
|
155
|
+
/** 最后使用时间。 */
|
|
156
|
+
lastUsedTime?: string;
|
|
157
|
+
/** 禅道服务端配置。 */
|
|
158
|
+
serverConfig?: ServerConfig;
|
|
159
|
+
/** 客户端自定义配置。 */
|
|
160
|
+
config?: ZentaoProfileConfig;
|
|
161
|
+
/** 允许上层应用保存额外字段。 */
|
|
162
|
+
[key: string]: unknown;
|
|
163
|
+
}
|
|
164
|
+
/** 运行时返回的 profile,会额外带上 `account@server` 形式的 key。 */
|
|
165
|
+
export interface ZentaoProfileRecord extends ZentaoProfile {
|
|
166
|
+
key: string;
|
|
167
|
+
}
|
|
168
|
+
/** 本地 profile 存储文件或浏览器 localStorage 中的 JSON 结构。 */
|
|
169
|
+
export interface ZentaoProfilesStore {
|
|
170
|
+
/** 当前使用的 profile key。 */
|
|
171
|
+
currentProfile?: string;
|
|
172
|
+
/** 保存的 profile 列表。 */
|
|
173
|
+
profiles: ZentaoProfile[];
|
|
174
|
+
}
|
|
114
175
|
/** 模块动作类型:基础 CRUD 或自定义动作。 */
|
|
115
176
|
export type ModuleActionType = 'list' | 'get' | 'create' | 'update' | 'delete' | 'action';
|
|
116
177
|
/** 模块动作使用的 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[];
|