zentao-api 0.2.0-beta.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,3 +1,9 @@
1
+ /**
2
+ * SDK 已知错误码到默认消息的映射表。
3
+ *
4
+ * 每条消息允许带 `{key}` 占位符,由 {@link ZentaoError} 构造时使用 `replacements`
5
+ * 进行字面量替换。此对象使用 `as const`,可直接作为类型来源约束错误码。
6
+ */
1
7
  export const ERRORS = {
2
8
  E_INVALID_BASE_URL: 'Invalid ZenTao baseUrl.',
3
9
  E_NO_GLOBAL_CLIENT: 'No global client configured. Call ZentaoClient.init() or pass options.client.',
@@ -6,20 +12,39 @@ export const ERRORS = {
6
12
  E_TIMEOUT: 'Request timed out.',
7
13
  E_INSECURE_BROWSER: 'The insecure option is only supported in Node.js runtimes.',
8
14
  E_LOGIN_FAILED: 'ZenTao login failed.',
15
+ E_INVALID_PROFILE: 'Invalid ZenTao profile.',
16
+ E_NO_PROFILE: 'No ZenTao profile is configured.',
17
+ E_PROFILE_NOT_FOUND: 'ZenTao profile not found: {profileKey}',
18
+ E_PROFILE_STORAGE_INVALID: 'ZenTao profile storage is not valid JSON.',
19
+ E_PROFILE_STORAGE_UNAVAILABLE: 'ZenTao profile storage is unavailable in this runtime.',
9
20
  E_INVALID_MODULE: 'Unknown module: {module}',
10
21
  E_INVALID_ACTION: 'Unknown action: {module}-{action}',
11
22
  E_INVALID_MODULE_DEFINITION: 'Invalid module definition.',
12
23
  E_INVALID_ACTION_DEFINITION: 'Invalid module action definition.',
13
24
  E_MISSING_PARAM: 'Missing required parameter: {param}',
25
+ E_INVALID_PARAM: 'Invalid value for parameter {param}: {value}',
14
26
  E_INVALID_REQUEST_NAME: 'Request name must use the form "moduleName/methodName".',
27
+ E_API_FAILED: 'ZenTao API returned failure: {message}',
15
28
  };
16
- /** SDK 统一错误类型,所有可预期错误都会携带稳定错误码。 */
29
+ /**
30
+ * SDK 统一错误类型。
31
+ *
32
+ * 所有可预期错误(参数缺失、HTTP/网络/超时、登录失败、profile 异常等)都通过
33
+ * `ZentaoError` 抛出并携带稳定 {@link ErrorCode},方便调用方按 `code` 区分处理。
34
+ * 错误消息默认来自 {@link ERRORS} 中的模板,并支持占位符替换。
35
+ */
17
36
  export class ZentaoError extends Error {
18
- /** 错误码,对应 {@link ERRORS} 的 key */
37
+ /** 错误码,对应 {@link ERRORS} 的 key;用于稳定地区分错误类型。 */
19
38
  code;
20
- /** 附加上下文,例如 HTTP 响应详情或原始异常。 */
39
+ /** 附加上下文,例如 HTTP 响应详情、原始异常或失败的禅道响应原文。 */
21
40
  details;
22
- /** 根据错误码和占位符替换值创建错误。 */
41
+ /**
42
+ * 构造 SDK 错误实例。
43
+ *
44
+ * @param code - 错误码,必须是 {@link ERRORS} 中已声明的 key。
45
+ * @param replacements - 可选的占位符替换值;遍历后会把 `{key}` 替换为字符串化的值。
46
+ * @param details - 可选的附加上下文(HTTP 响应详情、原始异常等),保存到 {@link details}。
47
+ */
23
48
  constructor(code, replacements, details) {
24
49
  let message = ERRORS[code];
25
50
  if (replacements) {
@@ -1,5 +1,18 @@
1
1
  import type { GlobalOptions } from '../types/index.js';
2
- /** 获取当前全局选项快照;返回副本,避免调用方直接改写内部状态。 */
2
+ /**
3
+ * 获取当前全局选项的快照。
4
+ *
5
+ * 返回的是浅拷贝副本,对返回值的修改不会影响内部状态;如需更新请使用 {@link setGlobalOptions}。
6
+ *
7
+ * @returns 当前生效的全局选项快照,参见 {@link GlobalOptions}。
8
+ */
3
9
  export declare function getGlobalOptions(): GlobalOptions;
4
- /** 合并设置全局选项;传入 `undefined` 可清空对应字段。 */
10
+ /**
11
+ * 以浅合并的方式更新全局选项。
12
+ *
13
+ * 仅覆盖传入 `options` 中显式声明的字段;其余字段保留原值。若希望清空某个字段,
14
+ * 显式传入 `undefined` 即可(会写入 `undefined` 并在后续读取时返回 `undefined`)。
15
+ *
16
+ * @param options - 要合并到全局选项中的字段子集。
17
+ */
5
18
  export declare function setGlobalOptions(options: Partial<GlobalOptions>): void;
@@ -1,9 +1,22 @@
1
1
  let globalOptions = {};
2
- /** 获取当前全局选项快照;返回副本,避免调用方直接改写内部状态。 */
2
+ /**
3
+ * 获取当前全局选项的快照。
4
+ *
5
+ * 返回的是浅拷贝副本,对返回值的修改不会影响内部状态;如需更新请使用 {@link setGlobalOptions}。
6
+ *
7
+ * @returns 当前生效的全局选项快照,参见 {@link GlobalOptions}。
8
+ */
3
9
  export function getGlobalOptions() {
4
10
  return { ...globalOptions };
5
11
  }
6
- /** 合并设置全局选项;传入 `undefined` 可清空对应字段。 */
12
+ /**
13
+ * 以浅合并的方式更新全局选项。
14
+ *
15
+ * 仅覆盖传入 `options` 中显式声明的字段;其余字段保留原值。若希望清空某个字段,
16
+ * 显式传入 `undefined` 即可(会写入 `undefined` 并在后续读取时返回 `undefined`)。
17
+ *
18
+ * @param options - 要合并到全局选项中的字段子集。
19
+ */
7
20
  export function setGlobalOptions(options) {
8
21
  globalOptions = { ...globalOptions, ...options };
9
22
  }
@@ -2,21 +2,84 @@ import type { ModuleAction, ModuleDefinition } from '../types/index.js';
2
2
  import { BUILTIN_MODULES } from './generated.js';
3
3
  export { BUILTIN_MODULES };
4
4
  export declare const MODULES: ModuleDefinition[];
5
+ /** {@link defineModules} 的选项。 */
5
6
  export interface DefineModulesOptions {
6
- /** 同名模块是否整体替换;默认合并模块定义和动作。 */
7
- relace?: boolean;
7
+ /**
8
+ * 同名模块的写入策略。
9
+ *
10
+ * - `false`(默认):合并模块的元数据,并按动作名对动作做"同名替换、未知追加"。
11
+ * - `true`:整体替换已存在的模块定义,原有动作会被丢弃。
12
+ */
13
+ replace?: boolean;
8
14
  }
9
- /** 定义或扩展模块;同名模块默认合并动作,`relace` 为真时整体替换,未知模块追加。 */
15
+ /**
16
+ * 注册或扩展模块定义。
17
+ *
18
+ * 行为细节:
19
+ * - 模块名匹配大小写不敏感。
20
+ * - 未知模块直接追加到注册表末尾。
21
+ * - 已存在的模块默认按 `mergeModule` 合并:模块元数据浅合并、动作按名同名替换/未知追加;
22
+ * `options.replace` 为 `true` 时整体替换。
23
+ * - 所有写入都会做深克隆 + 深冻结:调用方后续修改自己的对象不会污染注册表,注册表也不可被外部改写。
24
+ *
25
+ * @param input - 单个或一组模块定义。
26
+ * @param options - 写入策略,参见 {@link DefineModulesOptions}。
27
+ * @throws {ZentaoError} `E_INVALID_MODULE_DEFINITION` —— 缺少 `name` 或 `actions` 字段。
28
+ */
10
29
  export declare function defineModules(input: ModuleDefinition | ModuleDefinition[], options?: DefineModulesOptions): void;
11
- /** 为已存在模块定义或覆盖动作;同名动作替换,未知动作追加。 */
30
+ /**
31
+ * 为已存在的模块追加或覆盖动作。
32
+ *
33
+ * 不做深度合并:同名动作整体替换,未知动作追加。这避免在 schema、参数数组等字段上出现隐式合并规则。
34
+ *
35
+ * @param moduleName - 目标模块名(大小写不敏感)。
36
+ * @param input - 单个或一组动作定义。
37
+ * @throws {ZentaoError} `E_INVALID_MODULE`(模块未注册)或 `E_INVALID_ACTION_DEFINITION`
38
+ * (动作缺少 `name` / `path` / `method` 等必填字段)。
39
+ */
12
40
  export declare function defineModuleActions(moduleName: string, input: ModuleAction | ModuleAction[]): void;
13
- /** 获取模块定义;模块不存在时抛出 {@link ZentaoError}。 */
41
+ /**
42
+ * 获取模块定义。
43
+ *
44
+ * 模块名匹配大小写不敏感。返回值是注册表内部的已深冻结引用(O(1) 查询、零拷贝),
45
+ * 任何写入尝试在严格模式下会抛 `TypeError`;如需修改请使用 {@link defineModules}。
46
+ *
47
+ * @param moduleName - 模块名。
48
+ * @returns 已注册的模块定义。
49
+ * @throws {ZentaoError} `E_INVALID_MODULE` —— 模块未注册。
50
+ */
14
51
  export declare function getModule(moduleName: string): ModuleDefinition;
15
- /** 获取指定模块动作;`ls` 会作为 `list` 的别名处理。 */
52
+ /**
53
+ * 获取指定模块下的某个动作。
54
+ *
55
+ * 解析顺序:
56
+ * 1. `actionName === 'ls'` 时映射为 `list`(仅作为别名,不会修改注册表)。
57
+ * 2. 在该模块的动作中按名称大小写不敏感匹配。
58
+ * 3. 当请求的动作不是基础 CRUD(`list`/`get`/`create`/`update`/`delete`)时,
59
+ * 额外允许命中 `type === 'action'` 的自定义动作(即使名字不在基础 CRUD 中)。
60
+ *
61
+ * 返回值同样是已深冻结的引用,请勿尝试修改。
62
+ *
63
+ * @param moduleName - 模块名(大小写不敏感)。
64
+ * @param actionName - 动作名(大小写不敏感);支持 `ls` 作为 `list` 的别名。
65
+ * @returns 匹配到的动作定义。
66
+ * @throws {ZentaoError} `E_INVALID_MODULE`(模块未注册)或 `E_INVALID_ACTION`(动作不存在)。
67
+ */
16
68
  export declare function getModuleAction(moduleName: string, actionName: string): ModuleAction;
17
- /** 返回当前运行时注册表中的所有模块名。 */
69
+ /**
70
+ * 返回当前运行时注册表中的所有模块名。
71
+ *
72
+ * 顺序与模块写入注册表的顺序一致;包括内置模块和通过 {@link defineModules} 追加的用户模块。
73
+ *
74
+ * @returns 模块名数组(保留原始大小写)。
75
+ */
18
76
  export declare function getModuleNames(): string[];
19
- /** 判断模块名是否已注册,大小写不敏感。 */
77
+ /**
78
+ * 判断模块名是否已注册。
79
+ *
80
+ * @param moduleName - 模块名;匹配大小写不敏感。
81
+ * @returns 已注册返回 `true`,否则 `false`。
82
+ */
20
83
  export declare function isModuleName(moduleName: string): boolean;
21
84
  /** @internal */
22
85
  export declare function resetModuleDefinitions(): void;
@@ -3,42 +3,70 @@ import { asArray } from '../utils/index.js';
3
3
  import { BUILTIN_MODULES } from './generated.js';
4
4
  export { BUILTIN_MODULES };
5
5
  export const MODULES = BUILTIN_MODULES;
6
- // 运行时注册表使用内置定义的浅克隆,避免用户扩展污染生成文件导出的常量。
7
- let modules = cloneModules(BUILTIN_MODULES);
6
+ // 运行时注册表存放「深克隆 + 深冻结」后的模块定义:
7
+ // - 深克隆:避免用户后续修改自己的输入对象时污染注册表;
8
+ // - 深冻结:让 getModule / getModuleAction 可以零拷贝返回引用,
9
+ // 外部尝试改写会在严格模式下抛 TypeError,开销也降到 O(1) 查询。
10
+ let modules = freezeModules(deepClone(BUILTIN_MODULES));
8
11
  let moduleMap = buildModuleMap(modules);
9
- function cloneActions(source) {
10
- return source.map((action) => ({ ...action }));
12
+ function deepClone(value) {
13
+ if (Array.isArray(value)) {
14
+ return value.map((item) => deepClone(item));
15
+ }
16
+ if (value && typeof value === 'object' && !(value instanceof Function)) {
17
+ const result = {};
18
+ for (const [key, nestedValue] of Object.entries(value)) {
19
+ result[key] = deepClone(nestedValue);
20
+ }
21
+ return result;
22
+ }
23
+ return value;
24
+ }
25
+ function deepFreeze(value) {
26
+ if (value === null || typeof value !== 'object')
27
+ return value;
28
+ if (Object.isFrozen(value))
29
+ return value;
30
+ for (const key of Object.keys(value)) {
31
+ deepFreeze(value[key]);
32
+ }
33
+ return Object.freeze(value);
11
34
  }
12
- function cloneModules(source) {
13
- return source.map((module) => ({
14
- ...module,
15
- actions: cloneActions(module.actions),
16
- }));
35
+ function freezeAction(action) {
36
+ return deepFreeze(action);
37
+ }
38
+ function freezeModule(module) {
39
+ module.actions.forEach(freezeAction);
40
+ return deepFreeze(module);
41
+ }
42
+ function freezeModules(source) {
43
+ source.forEach(freezeModule);
44
+ return source;
17
45
  }
18
46
  function findActionIndex(source, actionName) {
19
47
  const key = actionName.toLowerCase();
20
48
  return source.findIndex((action) => String(action.name).toLowerCase() === key);
21
49
  }
22
50
  function mergeActions(base, extension) {
23
- const next = cloneActions(base);
51
+ const next = base.slice();
24
52
  for (const action of extension) {
25
53
  const index = findActionIndex(next, String(action.name));
26
- const clone = { ...action };
54
+ const frozen = freezeAction(deepClone(action));
27
55
  if (index >= 0) {
28
- next[index] = clone;
56
+ next[index] = frozen;
29
57
  }
30
58
  else {
31
- next.push(clone);
59
+ next.push(frozen);
32
60
  }
33
61
  }
34
62
  return next;
35
63
  }
36
64
  function mergeModule(base, extension) {
37
- return {
65
+ return freezeModule({
38
66
  ...base,
39
- ...extension,
67
+ ...deepClone(extension),
40
68
  actions: mergeActions(base.actions, extension.actions),
41
- };
69
+ });
42
70
  }
43
71
  function buildModuleMap(source) {
44
72
  return new Map(source.map((module) => [module.name.toLowerCase(), module]));
@@ -56,42 +84,80 @@ function validateAction(action) {
56
84
  throw new ZentaoError('E_INVALID_ACTION_DEFINITION');
57
85
  }
58
86
  }
59
- /** 定义或扩展模块;同名模块默认合并动作,`relace` 为真时整体替换,未知模块追加。 */
87
+ /**
88
+ * 注册或扩展模块定义。
89
+ *
90
+ * 行为细节:
91
+ * - 模块名匹配大小写不敏感。
92
+ * - 未知模块直接追加到注册表末尾。
93
+ * - 已存在的模块默认按 `mergeModule` 合并:模块元数据浅合并、动作按名同名替换/未知追加;
94
+ * `options.replace` 为 `true` 时整体替换。
95
+ * - 所有写入都会做深克隆 + 深冻结:调用方后续修改自己的对象不会污染注册表,注册表也不可被外部改写。
96
+ *
97
+ * @param input - 单个或一组模块定义。
98
+ * @param options - 写入策略,参见 {@link DefineModulesOptions}。
99
+ * @throws {ZentaoError} `E_INVALID_MODULE_DEFINITION` —— 缺少 `name` 或 `actions` 字段。
100
+ */
60
101
  export function defineModules(input, options = {}) {
61
102
  for (const module of asArray(input)) {
62
103
  validateModule(module);
63
104
  const key = module.name.toLowerCase();
64
105
  const index = modules.findIndex((item) => item.name.toLowerCase() === key);
65
- const next = { ...module, actions: cloneActions(module.actions) };
66
106
  if (index >= 0) {
67
- modules[index] = options.relace ? next : mergeModule(modules[index], module);
107
+ modules[index] = options.replace
108
+ ? freezeModule(deepClone(module))
109
+ : mergeModule(modules[index], module);
68
110
  }
69
111
  else {
70
- modules.push(next);
112
+ modules.push(freezeModule(deepClone(module)));
71
113
  }
72
114
  }
73
115
  rebuildMap();
74
116
  }
75
- /** 为已存在模块定义或覆盖动作;同名动作替换,未知动作追加。 */
117
+ /**
118
+ * 为已存在的模块追加或覆盖动作。
119
+ *
120
+ * 不做深度合并:同名动作整体替换,未知动作追加。这避免在 schema、参数数组等字段上出现隐式合并规则。
121
+ *
122
+ * @param moduleName - 目标模块名(大小写不敏感)。
123
+ * @param input - 单个或一组动作定义。
124
+ * @throws {ZentaoError} `E_INVALID_MODULE`(模块未注册)或 `E_INVALID_ACTION_DEFINITION`
125
+ * (动作缺少 `name` / `path` / `method` 等必填字段)。
126
+ */
76
127
  export function defineModuleActions(moduleName, input) {
77
- const module = moduleMap.get(moduleName.toLowerCase());
128
+ const key = moduleName.toLowerCase();
129
+ const module = moduleMap.get(key);
78
130
  if (!module) {
79
131
  throw new ZentaoError('E_INVALID_MODULE', { module: moduleName });
80
132
  }
133
+ const actions = module.actions.slice();
81
134
  for (const action of asArray(input)) {
82
135
  validateAction(action);
83
- const index = findActionIndex(module.actions, String(action.name));
84
- const next = { ...action };
136
+ const index = findActionIndex(actions, String(action.name));
137
+ const frozen = freezeAction(deepClone(action));
85
138
  // 同名动作替换,未知动作追加;不做深度合并,避免 schema/数组字段出现隐式规则。
86
139
  if (index >= 0) {
87
- module.actions[index] = next;
140
+ actions[index] = frozen;
88
141
  }
89
142
  else {
90
- module.actions.push(next);
143
+ actions.push(frozen);
91
144
  }
92
145
  }
146
+ const nextModule = freezeModule({ ...module, actions });
147
+ const index = modules.findIndex((item) => item.name.toLowerCase() === key);
148
+ modules[index] = nextModule;
149
+ rebuildMap();
93
150
  }
94
- /** 获取模块定义;模块不存在时抛出 {@link ZentaoError}。 */
151
+ /**
152
+ * 获取模块定义。
153
+ *
154
+ * 模块名匹配大小写不敏感。返回值是注册表内部的已深冻结引用(O(1) 查询、零拷贝),
155
+ * 任何写入尝试在严格模式下会抛 `TypeError`;如需修改请使用 {@link defineModules}。
156
+ *
157
+ * @param moduleName - 模块名。
158
+ * @returns 已注册的模块定义。
159
+ * @throws {ZentaoError} `E_INVALID_MODULE` —— 模块未注册。
160
+ */
95
161
  export function getModule(moduleName) {
96
162
  const module = moduleMap.get(moduleName.toLowerCase());
97
163
  if (!module) {
@@ -99,7 +165,22 @@ export function getModule(moduleName) {
99
165
  }
100
166
  return module;
101
167
  }
102
- /** 获取指定模块动作;`ls` 会作为 `list` 的别名处理。 */
168
+ /**
169
+ * 获取指定模块下的某个动作。
170
+ *
171
+ * 解析顺序:
172
+ * 1. `actionName === 'ls'` 时映射为 `list`(仅作为别名,不会修改注册表)。
173
+ * 2. 在该模块的动作中按名称大小写不敏感匹配。
174
+ * 3. 当请求的动作不是基础 CRUD(`list`/`get`/`create`/`update`/`delete`)时,
175
+ * 额外允许命中 `type === 'action'` 的自定义动作(即使名字不在基础 CRUD 中)。
176
+ *
177
+ * 返回值同样是已深冻结的引用,请勿尝试修改。
178
+ *
179
+ * @param moduleName - 模块名(大小写不敏感)。
180
+ * @param actionName - 动作名(大小写不敏感);支持 `ls` 作为 `list` 的别名。
181
+ * @returns 匹配到的动作定义。
182
+ * @throws {ZentaoError} `E_INVALID_MODULE`(模块未注册)或 `E_INVALID_ACTION`(动作不存在)。
183
+ */
103
184
  export function getModuleAction(moduleName, actionName) {
104
185
  const module = getModule(moduleName);
105
186
  const normalized = actionName === 'ls' ? 'list' : actionName;
@@ -114,16 +195,27 @@ export function getModuleAction(moduleName, actionName) {
114
195
  }
115
196
  throw new ZentaoError('E_INVALID_ACTION', { module: moduleName, action: actionName });
116
197
  }
117
- /** 返回当前运行时注册表中的所有模块名。 */
198
+ /**
199
+ * 返回当前运行时注册表中的所有模块名。
200
+ *
201
+ * 顺序与模块写入注册表的顺序一致;包括内置模块和通过 {@link defineModules} 追加的用户模块。
202
+ *
203
+ * @returns 模块名数组(保留原始大小写)。
204
+ */
118
205
  export function getModuleNames() {
119
206
  return modules.map((module) => module.name);
120
207
  }
121
- /** 判断模块名是否已注册,大小写不敏感。 */
208
+ /**
209
+ * 判断模块名是否已注册。
210
+ *
211
+ * @param moduleName - 模块名;匹配大小写不敏感。
212
+ * @returns 已注册返回 `true`,否则 `false`。
213
+ */
122
214
  export function isModuleName(moduleName) {
123
215
  return moduleMap.has(moduleName.toLowerCase());
124
216
  }
125
217
  /** @internal */
126
218
  export function resetModuleDefinitions() {
127
- modules = cloneModules(BUILTIN_MODULES);
219
+ modules = freezeModules(deepClone(BUILTIN_MODULES));
128
220
  rebuildMap();
129
221
  }
@@ -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,23 +45,35 @@ 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
- }
48
+ const TRUTHY_STRINGS = new Set(['true', '1', 'yes', 'on']);
49
+ const FALSY_STRINGS = new Set(['false', '0', 'no', 'off']);
51
50
  /** 按 OpenAPI schema 的基础类型对参数做轻量转换。 */
52
- function coerceValue(value, type) {
53
- if (value === undefined)
54
- return undefined;
51
+ function coerceValue(value, type, paramName) {
52
+ if (value === undefined || value === null)
53
+ return value;
55
54
  if (type === 'number' || type === 'integer') {
56
55
  const numberValue = Number(value);
57
56
  return Number.isNaN(numberValue) ? value : numberValue;
58
57
  }
59
58
  if (type === 'boolean') {
60
- if (value === 'true')
61
- return true;
62
- if (value === 'false')
63
- return false;
64
- return Boolean(value);
59
+ if (typeof value === 'boolean')
60
+ return value;
61
+ if (typeof value === 'number') {
62
+ if (value === 1)
63
+ return true;
64
+ if (value === 0)
65
+ return false;
66
+ throw new ZentaoError('E_INVALID_PARAM', { param: paramName, value: String(value) });
67
+ }
68
+ if (typeof value === 'string') {
69
+ const normalized = value.trim().toLowerCase();
70
+ if (TRUTHY_STRINGS.has(normalized))
71
+ return true;
72
+ if (FALSY_STRINGS.has(normalized))
73
+ return false;
74
+ throw new ZentaoError('E_INVALID_PARAM', { param: paramName, value });
75
+ }
76
+ throw new ZentaoError('E_INVALID_PARAM', { param: paramName, value: String(value) });
65
77
  }
66
78
  return value;
67
79
  }
@@ -131,16 +143,17 @@ export function resolveModuleCommand(module, actionName, params = {}) {
131
143
  for (const [key, property] of Object.entries(schema.properties ?? {})) {
132
144
  // body 字段优先级:params.data 中的字段 > 平铺 params 字段 > schema 默认值。
133
145
  const hasDataValue = Object.prototype.hasOwnProperty.call(data, key);
134
- let value = data[key] ?? params[key] ?? property.defaultValue;
146
+ const hasParamValue = Object.prototype.hasOwnProperty.call(params, key);
147
+ let value = hasDataValue ? data[key] : hasParamValue ? params[key] : property.defaultValue;
135
148
  if (value === undefined && (property.required || required.has(key))) {
136
149
  throw new ZentaoError('E_MISSING_PARAM', { param: key });
137
150
  }
138
- value = coerceValue(value, property.type);
139
- if (property.type === 'array' && value !== undefined && !Array.isArray(value)) {
151
+ value = coerceValue(value, property.type, key);
152
+ if (property.type === 'array' && value !== undefined && value !== null && !Array.isArray(value)) {
140
153
  if (typeof value === 'string') {
141
154
  value = value.split(',');
142
155
  }
143
- else if (!hasDataValue || !isPlainObject(value)) {
156
+ else if (!hasDataValue || !isRecord(value)) {
144
157
  value = [value];
145
158
  }
146
159
  }
@@ -0,0 +1,76 @@
1
+ import type { ZentaoProfile, ZentaoProfileRecord } from '../types/index.js';
2
+ /**
3
+ * 浏览器环境下用于在 `localStorage` 中保存 profile 数据的 key。
4
+ *
5
+ * Node.js 环境会改用文件 `~/.config/zentao/zentao.json`,与此常量无关。
6
+ */
7
+ export declare const ZENTAO_PROFILES_STORAGE_KEY = "ZENTAO_PROFILES";
8
+ /**
9
+ * 根据 profile 的账号和禅道站点地址生成稳定 key。
10
+ *
11
+ * Key 格式为 `account@server`,其中 `server` 会经过 {@link normalizeSiteUrl} 规范化,
12
+ * 因此即使传入末尾带 `/` 或 `/api.php/v2` 的地址,也会得到一致的结果。
13
+ *
14
+ * @param profile - 只需要包含 `account` 和 `server` 两个字段。
15
+ * @returns 形如 `admin@https://zentao.example.com` 的 profile key。
16
+ * @throws {ZentaoError} `E_INVALID_PROFILE`(账号为空白)或 `E_INVALID_BASE_URL`(`server` 不合法)。
17
+ */
18
+ export declare function getProfileKey(profile: Pick<ZentaoProfile, 'account' | 'server'>): string;
19
+ /**
20
+ * 列出本地保存的所有 profile。
21
+ *
22
+ * Node.js 下从 `~/.config/zentao/zentao.json` 读取;浏览器下从 `localStorage` 读取。
23
+ * 读取过程不会写回存储;存储中无法解析的条目会被静默忽略,不会影响其余 profile。
24
+ *
25
+ * @returns 当前存储中的所有 profile(带 `key` 字段),文件不存在时返回空数组。
26
+ * @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID`(存储内容不是合法 JSON)或
27
+ * `E_PROFILE_STORAGE_UNAVAILABLE`(运行时无法访问存储)。
28
+ */
29
+ export declare function getAllProfiles(): Promise<ZentaoProfileRecord[]>;
30
+ /**
31
+ * 获取指定 profile。
32
+ *
33
+ * @param profileKey - 可选的 profile key(`account@server`);不传时返回当前(最近一次切换的)profile。
34
+ * @returns 命中的 profile(带 `key` 字段);当 key 不存在或尚未配置当前 profile 时返回 `undefined`。
35
+ * @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID` / `E_PROFILE_STORAGE_UNAVAILABLE`。
36
+ */
37
+ export declare function getProfile(profileKey?: string): Promise<ZentaoProfileRecord | undefined>;
38
+ /**
39
+ * 添加或覆盖一个本地 profile,并把它设置为当前使用的 profile。
40
+ *
41
+ * 行为细节:
42
+ * - 同 key(`account@server`)已存在时会**整体覆盖**而非合并字段。
43
+ * - 写入时会自动补齐 `loginTime` 与 `lastUsedTime`(若调用方未提供则使用当前 ISO 时间)。
44
+ * - 操作通过进程内串行锁保护 read-modify-write,避免并发调用导致的 lost update;跨进程并发不在保证范围。
45
+ * - 实际写入使用临时文件 + `rename` 的原子方式,并将文件与目录权限收紧到 `0600`/`0700`(Node.js 下)。
46
+ *
47
+ * @param profile - 要写入的 profile,必须至少包含 `server`、`account`、`token`。
48
+ * @returns 实际写入并附带 `key` 字段的 profile 记录。
49
+ * @throws {ZentaoError} `E_INVALID_PROFILE`(必填字段缺失或 token 为空白)、
50
+ * `E_INVALID_BASE_URL`、`E_PROFILE_STORAGE_INVALID`、`E_PROFILE_STORAGE_UNAVAILABLE`。
51
+ */
52
+ export declare function addProfile(profile: ZentaoProfile): Promise<ZentaoProfileRecord>;
53
+ /**
54
+ * 删除指定 profile。
55
+ *
56
+ * 若被删除的是当前 profile,会回退为列表中最近一次写入的 profile;若已无任何 profile,
57
+ * 当前 profile 会被清空。操作同样通过进程内串行锁保护。
58
+ *
59
+ * @param profileKey - 要删除的 profile key。
60
+ * @returns 当且仅当确实删除了某条记录时返回 `true`;key 不存在时返回 `false` 且不会写盘。
61
+ * @throws {ZentaoError} `E_PROFILE_STORAGE_INVALID` / `E_PROFILE_STORAGE_UNAVAILABLE`。
62
+ */
63
+ export declare function deleteProfile(profileKey: string): Promise<boolean>;
64
+ /**
65
+ * 切换当前使用的 profile,并刷新其 `lastUsedTime`。
66
+ *
67
+ * 不传 `profileKey` 时使用当前 profile(相当于把当前 profile 的 `lastUsedTime` 刷新一遍)。
68
+ * 切换成功后会立即写回存储,由进程内串行锁保护。
69
+ *
70
+ * @param profileKey - 可选的目标 profile key;不传则使用当前 profile。
71
+ * @returns 切换后生效的 profile 记录(带 `key` 字段)。
72
+ * @throws {ZentaoError} `E_NO_PROFILE`(未配置任何当前 profile 且未传 key)、
73
+ * `E_PROFILE_NOT_FOUND`(目标 key 不存在)、`E_PROFILE_STORAGE_INVALID` /
74
+ * `E_PROFILE_STORAGE_UNAVAILABLE`。
75
+ */
76
+ export declare function switchProfile(profileKey?: string): Promise<ZentaoProfileRecord>;