meeglesdk 0.1.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.
Files changed (190) hide show
  1. package/README.md +191 -0
  2. package/dist/client.d.ts +186 -0
  3. package/dist/client.d.ts.map +1 -0
  4. package/dist/client.js +197 -0
  5. package/dist/client.js.map +1 -0
  6. package/dist/core/base-service.d.ts +156 -0
  7. package/dist/core/base-service.d.ts.map +1 -0
  8. package/dist/core/base-service.js +240 -0
  9. package/dist/core/base-service.js.map +1 -0
  10. package/dist/core/errors.d.ts +181 -0
  11. package/dist/core/errors.d.ts.map +1 -0
  12. package/dist/core/errors.js +250 -0
  13. package/dist/core/errors.js.map +1 -0
  14. package/dist/core/rate-limiter.d.ts +72 -0
  15. package/dist/core/rate-limiter.d.ts.map +1 -0
  16. package/dist/core/rate-limiter.js +269 -0
  17. package/dist/core/rate-limiter.js.map +1 -0
  18. package/dist/core/request.d.ts +270 -0
  19. package/dist/core/request.d.ts.map +1 -0
  20. package/dist/core/request.js +842 -0
  21. package/dist/core/request.js.map +1 -0
  22. package/dist/core/token-manager.d.ts +134 -0
  23. package/dist/core/token-manager.d.ts.map +1 -0
  24. package/dist/core/token-manager.js +412 -0
  25. package/dist/core/token-manager.js.map +1 -0
  26. package/dist/helpers/auth.d.ts +7 -0
  27. package/dist/helpers/auth.d.ts.map +1 -0
  28. package/dist/helpers/auth.js +19 -0
  29. package/dist/helpers/auth.js.map +1 -0
  30. package/dist/index.d.ts +72 -0
  31. package/dist/index.d.ts.map +1 -0
  32. package/dist/index.js +61 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/service/auth/index.d.ts +96 -0
  35. package/dist/service/auth/index.d.ts.map +1 -0
  36. package/dist/service/auth/index.js +163 -0
  37. package/dist/service/auth/index.js.map +1 -0
  38. package/dist/service/config/basic.d.ts +37 -0
  39. package/dist/service/config/basic.d.ts.map +1 -0
  40. package/dist/service/config/basic.js +54 -0
  41. package/dist/service/config/basic.js.map +1 -0
  42. package/dist/service/config/config.d.ts +27 -0
  43. package/dist/service/config/config.d.ts.map +1 -0
  44. package/dist/service/config/config.js +34 -0
  45. package/dist/service/config/config.js.map +1 -0
  46. package/dist/service/config/field.d.ts +50 -0
  47. package/dist/service/config/field.d.ts.map +1 -0
  48. package/dist/service/config/field.js +74 -0
  49. package/dist/service/config/field.js.map +1 -0
  50. package/dist/service/config/relation.d.ts +53 -0
  51. package/dist/service/config/relation.d.ts.map +1 -0
  52. package/dist/service/config/relation.js +66 -0
  53. package/dist/service/config/relation.js.map +1 -0
  54. package/dist/service/config/resource.d.ts +59 -0
  55. package/dist/service/config/resource.d.ts.map +1 -0
  56. package/dist/service/config/resource.js +75 -0
  57. package/dist/service/config/resource.js.map +1 -0
  58. package/dist/service/config/role.d.ts +55 -0
  59. package/dist/service/config/role.d.ts.map +1 -0
  60. package/dist/service/config/role.js +90 -0
  61. package/dist/service/config/role.js.map +1 -0
  62. package/dist/service/config/template.d.ts +65 -0
  63. package/dist/service/config/template.d.ts.map +1 -0
  64. package/dist/service/config/template.js +95 -0
  65. package/dist/service/config/template.js.map +1 -0
  66. package/dist/service/measure/index.d.ts +26 -0
  67. package/dist/service/measure/index.d.ts.map +1 -0
  68. package/dist/service/measure/index.js +36 -0
  69. package/dist/service/measure/index.js.map +1 -0
  70. package/dist/service/measure/query.d.ts +56 -0
  71. package/dist/service/measure/query.d.ts.map +1 -0
  72. package/dist/service/measure/query.js +86 -0
  73. package/dist/service/measure/query.js.map +1 -0
  74. package/dist/service/space/relation.d.ts +63 -0
  75. package/dist/service/space/relation.d.ts.map +1 -0
  76. package/dist/service/space/relation.js +102 -0
  77. package/dist/service/space/relation.js.map +1 -0
  78. package/dist/service/space/space.d.ts +81 -0
  79. package/dist/service/space/space.d.ts.map +1 -0
  80. package/dist/service/space/space.js +110 -0
  81. package/dist/service/space/space.js.map +1 -0
  82. package/dist/service/tenant/tenant.d.ts +52 -0
  83. package/dist/service/tenant/tenant.d.ts.map +1 -0
  84. package/dist/service/tenant/tenant.js +75 -0
  85. package/dist/service/tenant/tenant.js.map +1 -0
  86. package/dist/service/user/group.d.ts +47 -0
  87. package/dist/service/user/group.d.ts.map +1 -0
  88. package/dist/service/user/group.js +70 -0
  89. package/dist/service/user/group.js.map +1 -0
  90. package/dist/service/user/query.d.ts +25 -0
  91. package/dist/service/user/query.d.ts.map +1 -0
  92. package/dist/service/user/query.js +26 -0
  93. package/dist/service/user/query.js.map +1 -0
  94. package/dist/service/user/search.d.ts +25 -0
  95. package/dist/service/user/search.d.ts.map +1 -0
  96. package/dist/service/user/search.js +26 -0
  97. package/dist/service/user/search.js.map +1 -0
  98. package/dist/service/user/user.d.ts +37 -0
  99. package/dist/service/user/user.d.ts.map +1 -0
  100. package/dist/service/user/user.js +46 -0
  101. package/dist/service/user/user.js.map +1 -0
  102. package/dist/service/view/query.d.ts +52 -0
  103. package/dist/service/view/query.d.ts.map +1 -0
  104. package/dist/service/view/query.js +76 -0
  105. package/dist/service/view/query.js.map +1 -0
  106. package/dist/service/view/view.d.ts +76 -0
  107. package/dist/service/view/view.d.ts.map +1 -0
  108. package/dist/service/view/view.js +108 -0
  109. package/dist/service/view/view.js.map +1 -0
  110. package/dist/service/workitem/attachment.d.ts +143 -0
  111. package/dist/service/workitem/attachment.d.ts.map +1 -0
  112. package/dist/service/workitem/attachment.js +231 -0
  113. package/dist/service/workitem/attachment.js.map +1 -0
  114. package/dist/service/workitem/batch.d.ts +135 -0
  115. package/dist/service/workitem/batch.d.ts.map +1 -0
  116. package/dist/service/workitem/batch.js +146 -0
  117. package/dist/service/workitem/batch.js.map +1 -0
  118. package/dist/service/workitem/chat.d.ts +42 -0
  119. package/dist/service/workitem/chat.d.ts.map +1 -0
  120. package/dist/service/workitem/chat.js +50 -0
  121. package/dist/service/workitem/chat.js.map +1 -0
  122. package/dist/service/workitem/comment.d.ts +139 -0
  123. package/dist/service/workitem/comment.d.ts.map +1 -0
  124. package/dist/service/workitem/comment.js +180 -0
  125. package/dist/service/workitem/comment.js.map +1 -0
  126. package/dist/service/workitem/review.d.ts +44 -0
  127. package/dist/service/workitem/review.d.ts.map +1 -0
  128. package/dist/service/workitem/review.js +49 -0
  129. package/dist/service/workitem/review.js.map +1 -0
  130. package/dist/service/workitem/search.d.ts +213 -0
  131. package/dist/service/workitem/search.d.ts.map +1 -0
  132. package/dist/service/workitem/search.js +242 -0
  133. package/dist/service/workitem/search.js.map +1 -0
  134. package/dist/service/workitem/subtask.d.ts +193 -0
  135. package/dist/service/workitem/subtask.d.ts.map +1 -0
  136. package/dist/service/workitem/subtask.js +247 -0
  137. package/dist/service/workitem/subtask.js.map +1 -0
  138. package/dist/service/workitem/workItem.d.ts +225 -0
  139. package/dist/service/workitem/workItem.d.ts.map +1 -0
  140. package/dist/service/workitem/workItem.js +310 -0
  141. package/dist/service/workitem/workItem.js.map +1 -0
  142. package/dist/service/workitem/workflow.d.ts +214 -0
  143. package/dist/service/workitem/workflow.d.ts.map +1 -0
  144. package/dist/service/workitem/workflow.js +281 -0
  145. package/dist/service/workitem/workflow.js.map +1 -0
  146. package/dist/service/workitem/workhour.d.ts +63 -0
  147. package/dist/service/workitem/workhour.d.ts.map +1 -0
  148. package/dist/service/workitem/workhour.js +93 -0
  149. package/dist/service/workitem/workhour.js.map +1 -0
  150. package/dist/types/auth.d.ts +115 -0
  151. package/dist/types/auth.d.ts.map +1 -0
  152. package/dist/types/auth.js +6 -0
  153. package/dist/types/auth.js.map +1 -0
  154. package/dist/types/common.d.ts +354 -0
  155. package/dist/types/common.d.ts.map +1 -0
  156. package/dist/types/common.js +14 -0
  157. package/dist/types/common.js.map +1 -0
  158. package/dist/types/config.d.ts +305 -0
  159. package/dist/types/config.d.ts.map +1 -0
  160. package/dist/types/config.js +6 -0
  161. package/dist/types/config.js.map +1 -0
  162. package/dist/types/index.d.ts +13 -0
  163. package/dist/types/index.d.ts.map +1 -0
  164. package/dist/types/index.js +13 -0
  165. package/dist/types/index.js.map +1 -0
  166. package/dist/types/measure.d.ts +55 -0
  167. package/dist/types/measure.d.ts.map +1 -0
  168. package/dist/types/measure.js +6 -0
  169. package/dist/types/measure.js.map +1 -0
  170. package/dist/types/space.d.ts +164 -0
  171. package/dist/types/space.d.ts.map +1 -0
  172. package/dist/types/space.js +6 -0
  173. package/dist/types/space.js.map +1 -0
  174. package/dist/types/tenant.d.ts +106 -0
  175. package/dist/types/tenant.d.ts.map +1 -0
  176. package/dist/types/tenant.js +6 -0
  177. package/dist/types/tenant.js.map +1 -0
  178. package/dist/types/user.d.ts +110 -0
  179. package/dist/types/user.d.ts.map +1 -0
  180. package/dist/types/user.js +6 -0
  181. package/dist/types/user.js.map +1 -0
  182. package/dist/types/view.d.ts +162 -0
  183. package/dist/types/view.d.ts.map +1 -0
  184. package/dist/types/view.js +6 -0
  185. package/dist/types/view.js.map +1 -0
  186. package/dist/types/workitem.d.ts +1523 -0
  187. package/dist/types/workitem.d.ts.map +1 -0
  188. package/dist/types/workitem.js +6 -0
  189. package/dist/types/workitem.js.map +1 -0
  190. package/package.json +43 -0
@@ -0,0 +1,842 @@
1
+ /**
2
+ * HTTP 请求处理器
3
+ * 100% 参考 Meego API 文档
4
+ *
5
+ * 请求头规范:
6
+ * - Content-Type: application/json
7
+ * - X-Plugin-Token: {plugin_access_token | user_access_token}
8
+ * - X-User-Key: {user_key} (使用 plugin_token 时必须)
9
+ * - X-IDEM-UUID: {unique-id} (幂等标识,可选)
10
+ */
11
+ import { MeegoError, MeegoAuthError, MeegoNetworkError, MeegoTimeoutError, MeegoAbortError, ErrorCodes, } from './errors.js';
12
+ import { RateLimitManager, normalizeRateLimitOptions } from './rate-limiter.js';
13
+ class HttpStatusError extends Error {
14
+ status;
15
+ constructor(status, statusText) {
16
+ super(`HTTP error: ${status} ${statusText}`);
17
+ this.name = 'HttpStatusError';
18
+ this.status = status;
19
+ Object.setPrototypeOf(this, HttpStatusError.prototype);
20
+ }
21
+ }
22
+ /** 默认超时 30 秒 */
23
+ const DEFAULT_TIMEOUT = 30000;
24
+ /** 默认上传大小上限 100MB (10^6) */
25
+ const DEFAULT_UPLOAD_MAX_BYTES = 100_000_000;
26
+ /** 默认上传大小下限 1 字节(禁空文件) */
27
+ const DEFAULT_UPLOAD_MIN_BYTES = 1;
28
+ /** 默认最大重试延迟 30 秒 */
29
+ const DEFAULT_MAX_RETRY_DELAY = 30000;
30
+ /** 默认重试配置 */
31
+ const DEFAULT_RETRY_CONFIG = {
32
+ maxRetries: 2,
33
+ retryDelay: 1000,
34
+ maxRetryDelay: DEFAULT_MAX_RETRY_DELAY,
35
+ retryableErrorCodes: [
36
+ ErrorCodes.API_REQUEST_FREQUENCY_LIMIT, // 10429
37
+ ErrorCodes.RPC_CALL_ERROR, // 50006
38
+ ],
39
+ };
40
+ export class RequestHandler {
41
+ baseURL;
42
+ tokenManager;
43
+ logger;
44
+ timeout;
45
+ retryConfig;
46
+ rateLimitManager;
47
+ constructor(options) {
48
+ this.baseURL = options.baseURL.replace(/\/$/, ''); // 移除尾部斜杠
49
+ this.tokenManager = options.tokenManager;
50
+ this.logger = options.logger;
51
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
52
+ this.retryConfig = {
53
+ ...DEFAULT_RETRY_CONFIG,
54
+ ...options.retry,
55
+ };
56
+ const rateLimitOptions = normalizeRateLimitOptions(options.rateLimit);
57
+ if (rateLimitOptions && rateLimitOptions.enabled !== false) {
58
+ this.rateLimitManager = new RateLimitManager(rateLimitOptions);
59
+ }
60
+ }
61
+ /**
62
+ * 发送请求
63
+ */
64
+ async request(config) {
65
+ return this.executeWithRetry(config, (context) => this.executeOnce(context, (response) => this.parseDataResponse(response, config.responseValidator)));
66
+ }
67
+ /**
68
+ * 发送分页请求
69
+ * 用于 pagination 与 data 同级的接口(如子任务搜索)
70
+ */
71
+ async requestPaginated(config) {
72
+ return this.executeWithRetry(config, (context) => this.executeOnce(context, (response) => this.parsePaginatedResponse(response, config.responseValidator)));
73
+ }
74
+ /**
75
+ * 发送请求(自定义响应字段)
76
+ * 用于响应数据字段不是 data 的接口
77
+ */
78
+ async requestWithResponseKey(config, responseKey) {
79
+ return this.executeWithRetry(config, (context) => this.executeOnce(context, (response) => this.parseResponseKeyResponse(response, responseKey, config.responseValidator)));
80
+ }
81
+ /**
82
+ * 发送分页请求(data 为对象,pagination 与 data 同级)
83
+ */
84
+ async requestWithPagination(config) {
85
+ return this.executeWithRetry(config, (context) => this.executeOnce(context, (response) => this.parsePaginatedObjectResponse(response, config.responseValidator)));
86
+ }
87
+ /**
88
+ * 构建请求头
89
+ */
90
+ async buildHeaders(auth, idempotencyKey) {
91
+ const headers = await this.buildAuthHeaders(auth);
92
+ headers['Content-Type'] = 'application/json';
93
+ // 幂等键
94
+ if (idempotencyKey) {
95
+ headers['X-IDEM-UUID'] = idempotencyKey;
96
+ }
97
+ return headers;
98
+ }
99
+ /**
100
+ * 解析路径参数
101
+ * 例如: /open_api/:project_key/work_item -> /open_api/my-project/work_item
102
+ */
103
+ resolvePath(path, params) {
104
+ this.validatePathParams(path, params);
105
+ if (!params) {
106
+ return path;
107
+ }
108
+ let resolvedPath = path;
109
+ for (const [key, value] of Object.entries(params)) {
110
+ const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
111
+ resolvedPath = resolvedPath.replace(new RegExp(`:${escapedKey}`, 'g'), encodeURIComponent(String(value)));
112
+ }
113
+ return resolvedPath;
114
+ }
115
+ validatePathParams(path, params) {
116
+ const matches = Array.from(path.matchAll(/:([^/]+)/g));
117
+ if (matches.length === 0) {
118
+ return;
119
+ }
120
+ const requiredKeys = matches.map((match) => match[1]);
121
+ const missingKeys = [];
122
+ const emptyKeys = [];
123
+ for (const key of requiredKeys) {
124
+ const value = params?.[key];
125
+ if (value === undefined || value === null) {
126
+ missingKeys.push(key);
127
+ }
128
+ else if (String(value).length === 0) {
129
+ emptyKeys.push(key);
130
+ }
131
+ }
132
+ if (missingKeys.length > 0) {
133
+ const message = `Missing required path params: ${missingKeys.join(', ')}`;
134
+ throw new MeegoError(`[${ErrorCodes.MISSING_PARAM}] ${message}`, ErrorCodes.MISSING_PARAM, message, { missing: missingKeys });
135
+ }
136
+ if (emptyKeys.length > 0) {
137
+ const message = `Empty path params: ${emptyKeys.join(', ')}`;
138
+ throw new MeegoError(`[${ErrorCodes.INVALID_PARAM}] ${message}`, ErrorCodes.INVALID_PARAM, message, { empty: emptyKeys });
139
+ }
140
+ }
141
+ /**
142
+ * 构建查询字符串
143
+ */
144
+ buildQueryString(query) {
145
+ const params = new URLSearchParams();
146
+ for (const [key, value] of Object.entries(query)) {
147
+ if (value === undefined || value === null) {
148
+ continue;
149
+ }
150
+ if (Array.isArray(value)) {
151
+ // 数组参数
152
+ for (const item of value) {
153
+ params.append(key, String(item));
154
+ }
155
+ }
156
+ else if (typeof value === 'object') {
157
+ // 对象参数,JSON 序列化
158
+ params.append(key, JSON.stringify(value));
159
+ }
160
+ else {
161
+ params.append(key, String(value));
162
+ }
163
+ }
164
+ return params.toString();
165
+ }
166
+ /**
167
+ * 判断是否应该重试
168
+ */
169
+ shouldRetry(error, retryableErrorCodes) {
170
+ if (error instanceof MeegoAbortError) {
171
+ return false;
172
+ }
173
+ if (error instanceof MeegoError) {
174
+ return retryableErrorCodes.includes(error.errCode);
175
+ }
176
+ if (error instanceof MeegoNetworkError) {
177
+ // 网络错误可以重试
178
+ return true;
179
+ }
180
+ if (error instanceof MeegoTimeoutError) {
181
+ // 超时可以重试
182
+ return true;
183
+ }
184
+ return false;
185
+ }
186
+ /**
187
+ * 睡眠
188
+ */
189
+ sleep(ms) {
190
+ return new Promise((resolve) => setTimeout(resolve, ms));
191
+ }
192
+ /**
193
+ * 安全解析 JSON,将大整数转换为字符串
194
+ * 解决 JavaScript Number 无法精确表示超过 2^53-1 的整数问题
195
+ *
196
+ * 处理场景:
197
+ * - 对象属性值: {"id": 1234567890123456789}
198
+ * - 数组元素: [1234567890123456789]
199
+ * - 嵌套数组: [[1234567890123456789]]
200
+ * - 负数大整数: -1234567890123456789
201
+ */
202
+ parseJsonSafely(text) {
203
+ // 匹配 JSON 中的大整数(16位及以上),并转换为字符串
204
+ // 覆盖场景:
205
+ // - 根级数字(无前缀)
206
+ // - 对象属性值/数组元素
207
+ // - 科学计数法(仅处理整数形式且指数为正)
208
+ const bigIntPattern = /(^|[{\[:,])(\s*)(-?\d{16,})(?=\s*(?:[,\]\}]|$))/g;
209
+ const scientificPattern = /(^|[{\[:,])(\s*)(-?\d+(?:\.\d+)?[eE][+-]?\d+)(?=\s*(?:[,\]\}]|$))/g;
210
+ const wrapScientific = (value) => {
211
+ if (value.includes('.')) {
212
+ return false;
213
+ }
214
+ const parts = value.split(/e/i);
215
+ if (parts.length !== 2) {
216
+ return false;
217
+ }
218
+ const exp = Number.parseInt(parts[1], 10);
219
+ if (!Number.isFinite(exp) || exp < 0) {
220
+ return false;
221
+ }
222
+ const digits = parts[0].replace(/^-/, '');
223
+ const totalDigits = digits.length + exp;
224
+ return totalDigits >= 16;
225
+ };
226
+ const safeText = text
227
+ .replace(bigIntPattern, '$1$2"$3"')
228
+ .replace(scientificPattern, (match, prefix, spacing, value) => wrapScientific(value) ? `${prefix}${spacing}"${value}"` : match);
229
+ return JSON.parse(safeText);
230
+ }
231
+ getUploadFileSize(file) {
232
+ if (Buffer.isBuffer(file)) {
233
+ return file.byteLength;
234
+ }
235
+ const maybeSize = file.size;
236
+ return typeof maybeSize === 'number' && Number.isFinite(maybeSize) ? maybeSize : undefined;
237
+ }
238
+ /**
239
+ * 上传文件
240
+ * 使用 multipart/form-data 格式
241
+ */
242
+ async upload(config) {
243
+ const { path, pathParams, file, fileName, fieldName = 'file', formFields, auth, timeout, maxUploadSizeBytes, minUploadSizeBytes, skipRetry, retry, responseValidator, } = config;
244
+ const fileSize = this.getUploadFileSize(file);
245
+ const maxBytes = maxUploadSizeBytes ?? DEFAULT_UPLOAD_MAX_BYTES;
246
+ const minBytes = minUploadSizeBytes ?? DEFAULT_UPLOAD_MIN_BYTES;
247
+ if (typeof fileSize === 'number' && fileSize < minBytes) {
248
+ const minMessage = minBytes === DEFAULT_UPLOAD_MIN_BYTES
249
+ ? 'Uploaded File Size Must Be Greater Than 0'
250
+ : `Uploaded File Size Must Be At Least ${minBytes} bytes`;
251
+ throw new MeegoError(`[${ErrorCodes.INVALID_PARAM}] ${minMessage}`, ErrorCodes.INVALID_PARAM, minMessage, { code: ErrorCodes.INVALID_PARAM, msg: minMessage });
252
+ }
253
+ if (typeof fileSize === 'number' && fileSize > maxBytes) {
254
+ const limitMessage = maxBytes === DEFAULT_UPLOAD_MAX_BYTES
255
+ ? 'Uploaded File Size Limit 100M'
256
+ : `Uploaded File Size Limit ${maxBytes} bytes`;
257
+ throw new MeegoError(`[${ErrorCodes.UPLOAD_FILE_SIZE_LIMIT_100M}] ${limitMessage}`, ErrorCodes.UPLOAD_FILE_SIZE_LIMIT_100M, limitMessage, { code: ErrorCodes.UPLOAD_FILE_SIZE_LIMIT_100M, msg: limitMessage });
258
+ }
259
+ const resolvedPath = this.resolvePath(path, pathParams);
260
+ const url = this.buildUrlWithResolvedPath(resolvedPath);
261
+ // 构建 FormData
262
+ const formData = new FormData();
263
+ // 添加文件
264
+ // 处理 Buffer (Node.js) 和 Blob/File (浏览器/Bun) 的兼容性
265
+ if (Buffer.isBuffer(file)) {
266
+ // Buffer 可能是底层 ArrayBuffer 的部分视图,需要使用正确的 offset 切片
267
+ // 使用 slice 获取独立的 ArrayBuffer 以确保兼容性
268
+ // 注意:Node.js Buffer 的 buffer 属性可能是 SharedArrayBuffer,但 slice 返回的是 ArrayBuffer
269
+ const arrayBuffer = file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
270
+ formData.append(fieldName, new Blob([arrayBuffer]), fileName);
271
+ }
272
+ else {
273
+ formData.append(fieldName, file, fileName);
274
+ }
275
+ // 添加额外字段
276
+ if (formFields) {
277
+ for (const [key, value] of Object.entries(formFields)) {
278
+ formData.append(key, value);
279
+ }
280
+ }
281
+ // 构建请求头 (不设置 Content-Type,让浏览器/运行时自动设置)
282
+ const headers = await this.buildHeadersForUpload(auth);
283
+ const rateLimitContext = this.rateLimitManager
284
+ ? { token: headers['X-Plugin-Token'], method: 'POST', path: resolvedPath }
285
+ : undefined;
286
+ const context = {
287
+ url,
288
+ requestInit: {
289
+ method: 'POST',
290
+ headers,
291
+ body: formData,
292
+ },
293
+ timeout: timeout ?? this.timeout,
294
+ rateLimitContext,
295
+ };
296
+ return this.executeWithRetryContext(context, { skipRetry, retry }, () => this.executeOnce(context, (response) => this.parseDataResponse(response, responseValidator), 'Upload failed'));
297
+ }
298
+ /**
299
+ * 下载文件
300
+ * 返回二进制数据 (ArrayBuffer)
301
+ */
302
+ async download(config) {
303
+ const { path, pathParams, body, auth, timeout, skipRetry, retry, signal, onProgress, onChunk } = config;
304
+ const resolvedPath = this.resolvePath(path, pathParams);
305
+ const url = this.buildUrlWithResolvedPath(resolvedPath);
306
+ // 构建请求头
307
+ const headers = await this.buildHeaders(auth, undefined);
308
+ const rateLimitContext = this.rateLimitManager
309
+ ? { token: headers['X-Plugin-Token'], method: 'POST', path: resolvedPath }
310
+ : undefined;
311
+ const context = {
312
+ url,
313
+ requestInit: {
314
+ method: 'POST',
315
+ headers,
316
+ body: body ? JSON.stringify(body) : undefined,
317
+ },
318
+ timeout: timeout ?? this.timeout,
319
+ rateLimitContext,
320
+ signal,
321
+ };
322
+ const progressOptions = { onProgress, onChunk, signal };
323
+ return this.executeWithRetryContext(context, { skipRetry, retry }, () => this.executeOnce(context, (response) => this.parseDownloadResponse(response, progressOptions), 'Download failed'));
324
+ }
325
+ /**
326
+ * 下载文件(流式)
327
+ * 返回 ReadableStream<Uint8Array>
328
+ */
329
+ async downloadStream(config) {
330
+ const { path, pathParams, body, auth, timeout, skipRetry, retry, signal, onProgress, onChunk } = config;
331
+ const resolvedPath = this.resolvePath(path, pathParams);
332
+ const url = this.buildUrlWithResolvedPath(resolvedPath);
333
+ // 构建请求头
334
+ const headers = await this.buildHeaders(auth, undefined);
335
+ const rateLimitContext = this.rateLimitManager
336
+ ? { token: headers['X-Plugin-Token'], method: 'POST', path: resolvedPath }
337
+ : undefined;
338
+ const context = {
339
+ url,
340
+ requestInit: {
341
+ method: 'POST',
342
+ headers,
343
+ body: body ? JSON.stringify(body) : undefined,
344
+ },
345
+ timeout: timeout ?? this.timeout,
346
+ rateLimitContext,
347
+ signal,
348
+ };
349
+ const progressOptions = { onProgress, onChunk, signal };
350
+ return this.executeWithRetryContext(context, { skipRetry, retry }, () => this.executeOnce(context, (response) => this.parseDownloadStreamResponse(response, progressOptions), 'Download failed'));
351
+ }
352
+ /**
353
+ * 构建文件上传请求头 (不包含 Content-Type)
354
+ */
355
+ async buildHeadersForUpload(auth) {
356
+ return this.buildAuthHeaders(auth);
357
+ }
358
+ /**
359
+ * 构建鉴权请求头
360
+ */
361
+ async buildAuthHeaders(auth) {
362
+ const headers = {};
363
+ const context = auth ?? { type: 'auto' };
364
+ if (context.type === 'user') {
365
+ if (!context.token) {
366
+ throw new MeegoAuthError(0, 'User token not available');
367
+ }
368
+ headers['X-Plugin-Token'] = context.token;
369
+ return headers;
370
+ }
371
+ if (context.type === 'plugin') {
372
+ const token = context.token ?? (await this.tokenManager.getPluginToken());
373
+ headers['X-Plugin-Token'] = token;
374
+ if (context.userKey) {
375
+ headers['X-User-Key'] = context.userKey;
376
+ }
377
+ else {
378
+ this.logger.debug('Plugin auth used without userKey');
379
+ }
380
+ return headers;
381
+ }
382
+ const userKey = context.userKey;
383
+ if (userKey) {
384
+ const userToken = await this.tokenManager.getUserToken(userKey);
385
+ if (userToken) {
386
+ headers['X-Plugin-Token'] = userToken;
387
+ return headers;
388
+ }
389
+ const pluginToken = await this.tokenManager.getPluginToken();
390
+ headers['X-Plugin-Token'] = pluginToken;
391
+ headers['X-User-Key'] = userKey;
392
+ return headers;
393
+ }
394
+ const pluginToken = await this.tokenManager.getPluginToken();
395
+ headers['X-Plugin-Token'] = pluginToken;
396
+ return headers;
397
+ }
398
+ /**
399
+ * 构建请求上下文
400
+ */
401
+ async prepareRequest(config) {
402
+ const { method, path, pathParams, query, body, auth, idempotencyKey, signal } = config;
403
+ const resolvedPath = this.resolvePath(path, pathParams);
404
+ const url = this.buildUrlWithResolvedPath(resolvedPath, query);
405
+ const headers = await this.buildHeaders(auth, idempotencyKey);
406
+ const rateLimitContext = this.rateLimitManager
407
+ ? { token: headers['X-Plugin-Token'], method, path: resolvedPath }
408
+ : undefined;
409
+ const requestInit = {
410
+ method,
411
+ headers,
412
+ };
413
+ if (body !== undefined) {
414
+ requestInit.body = JSON.stringify(body);
415
+ }
416
+ return { url, requestInit, rateLimitContext, signal };
417
+ }
418
+ /**
419
+ * 执行带重试的请求
420
+ * @param config 基础请求配置(不含 responseValidator)
421
+ * @param executor 执行器函数,负责实际的请求解析
422
+ */
423
+ async executeWithRetry(config, executor) {
424
+ const { url, requestInit, rateLimitContext, signal } = await this.prepareRequest(config);
425
+ const context = {
426
+ url,
427
+ requestInit,
428
+ timeout: config.timeout ?? this.timeout,
429
+ rateLimitContext,
430
+ signal,
431
+ };
432
+ return this.executeWithRetryContext(context, { skipRetry: config.skipRetry, retry: config.retry }, () => executor(context));
433
+ }
434
+ async executeWithRetryContext(context, options, executor) {
435
+ const retryConfig = this.resolveRetryConfig(options.retry);
436
+ const maxAttempts = options.skipRetry ? 1 : retryConfig.maxRetries + 1;
437
+ let lastError;
438
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
439
+ try {
440
+ return await executor();
441
+ }
442
+ catch (error) {
443
+ lastError = error;
444
+ if (attempt < maxAttempts && this.shouldRetry(error, retryConfig.retryableErrorCodes)) {
445
+ const baseDelay = retryConfig.retryDelay * Math.pow(2, attempt - 1);
446
+ const maxDelay = typeof retryConfig.maxRetryDelay === 'number' && Number.isFinite(retryConfig.maxRetryDelay)
447
+ ? retryConfig.maxRetryDelay
448
+ : undefined;
449
+ const delay = maxDelay && maxDelay > 0 ? Math.min(baseDelay, maxDelay) : baseDelay;
450
+ this.logger.warn(`Request failed, retrying in ${delay}ms (attempt ${attempt}/${maxAttempts})`, {
451
+ url: context.url,
452
+ error: error.message,
453
+ });
454
+ await this.sleep(delay);
455
+ continue;
456
+ }
457
+ throw error;
458
+ }
459
+ }
460
+ throw lastError;
461
+ }
462
+ resolveRetryConfig(override) {
463
+ if (!override) {
464
+ return this.retryConfig;
465
+ }
466
+ return {
467
+ maxRetries: override.maxRetries ?? this.retryConfig.maxRetries,
468
+ retryDelay: override.retryDelay ?? this.retryConfig.retryDelay,
469
+ maxRetryDelay: override.maxRetryDelay ?? this.retryConfig.maxRetryDelay,
470
+ retryableErrorCodes: override.retryableErrorCodes ?? this.retryConfig.retryableErrorCodes,
471
+ };
472
+ }
473
+ /**
474
+ * 执行单次请求(统一超时与错误处理)
475
+ */
476
+ async executeOnce(context, parser, errorPrefix = 'Request failed') {
477
+ if (context.rateLimitContext && this.rateLimitManager) {
478
+ await this.rateLimitManager.acquire(context.rateLimitContext);
479
+ }
480
+ const controller = new AbortController();
481
+ let timedOut = false;
482
+ let abortedByUser = false;
483
+ const handleAbort = () => {
484
+ abortedByUser = true;
485
+ controller.abort();
486
+ };
487
+ if (context.signal) {
488
+ if (context.signal.aborted) {
489
+ abortedByUser = true;
490
+ controller.abort();
491
+ }
492
+ else {
493
+ context.signal.addEventListener('abort', handleAbort, { once: true });
494
+ }
495
+ }
496
+ const timeoutId = setTimeout(() => {
497
+ timedOut = true;
498
+ controller.abort();
499
+ }, context.timeout);
500
+ let timeoutCleared = false;
501
+ const clearTimeoutOnce = () => {
502
+ if (!timeoutCleared) {
503
+ clearTimeout(timeoutId);
504
+ timeoutCleared = true;
505
+ }
506
+ };
507
+ try {
508
+ this.logger.debug('Sending request', {
509
+ url: context.url,
510
+ method: context.requestInit.method,
511
+ });
512
+ const response = await fetch(context.url, {
513
+ ...context.requestInit,
514
+ signal: controller.signal,
515
+ });
516
+ clearTimeoutOnce();
517
+ return await parser(response);
518
+ }
519
+ catch (error) {
520
+ clearTimeoutOnce();
521
+ if (error instanceof MeegoError) {
522
+ throw error;
523
+ }
524
+ if (error instanceof MeegoNetworkError) {
525
+ throw error;
526
+ }
527
+ if (error instanceof MeegoTimeoutError) {
528
+ throw error;
529
+ }
530
+ if (error instanceof Error && error.name === 'AbortError') {
531
+ if (timedOut) {
532
+ throw new MeegoTimeoutError(context.timeout);
533
+ }
534
+ if (abortedByUser) {
535
+ throw new MeegoAbortError();
536
+ }
537
+ throw new MeegoNetworkError('Request aborted', error);
538
+ }
539
+ if (error instanceof HttpStatusError) {
540
+ throw new MeegoNetworkError(error.message, error);
541
+ }
542
+ throw new MeegoNetworkError(`${errorPrefix}: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
543
+ }
544
+ finally {
545
+ clearTimeoutOnce();
546
+ if (context.signal) {
547
+ context.signal.removeEventListener('abort', handleAbort);
548
+ }
549
+ }
550
+ }
551
+ /**
552
+ * 解析通用响应包裹
553
+ */
554
+ async parseEnvelope(response) {
555
+ const text = await response.text();
556
+ if (!text) {
557
+ if (!response.ok) {
558
+ throw new HttpStatusError(response.status, response.statusText);
559
+ }
560
+ return { err_code: 0, err_msg: 'success', err: {}, data: null };
561
+ }
562
+ let result;
563
+ try {
564
+ result = this.parseJsonSafely(text);
565
+ }
566
+ catch (error) {
567
+ throw new MeegoNetworkError(`Failed to parse response: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
568
+ }
569
+ if (typeof result.err_code !== 'number' || typeof result.err_msg !== 'string') {
570
+ if (!response.ok) {
571
+ throw new HttpStatusError(response.status, response.statusText);
572
+ }
573
+ throw new MeegoNetworkError('Invalid response');
574
+ }
575
+ if (result.err_code !== 0) {
576
+ throw new MeegoError(`[${result.err_code}] ${result.err_msg}`, result.err_code, result.err_msg, result.err, response.status);
577
+ }
578
+ if (!response.ok) {
579
+ throw new HttpStatusError(response.status, response.statusText);
580
+ }
581
+ return result;
582
+ }
583
+ validateResponse(data, validator) {
584
+ if (!validator) {
585
+ return data;
586
+ }
587
+ try {
588
+ const valid = validator(data);
589
+ if (!valid) {
590
+ throw new Error('Response validator returned false');
591
+ }
592
+ return data;
593
+ }
594
+ catch (error) {
595
+ throw new MeegoNetworkError(`Response validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
596
+ }
597
+ }
598
+ /**
599
+ * 验证数组响应的每个元素
600
+ * 用于分页响应,将单元素验证器应用于数组中的每个元素
601
+ */
602
+ validateArrayResponse(data, itemValidator) {
603
+ if (!Array.isArray(data)) {
604
+ throw new MeegoNetworkError('Response validation failed: Expected array');
605
+ }
606
+ if (!itemValidator) {
607
+ return data;
608
+ }
609
+ try {
610
+ for (let i = 0; i < data.length; i++) {
611
+ const valid = itemValidator(data[i]);
612
+ if (!valid) {
613
+ throw new Error(`Response validator returned false for item at index ${i}`);
614
+ }
615
+ }
616
+ return data;
617
+ }
618
+ catch (error) {
619
+ throw new MeegoNetworkError(`Response validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`, error instanceof Error ? error : undefined);
620
+ }
621
+ }
622
+ async parseDataResponse(response, validator) {
623
+ const result = await this.parseEnvelope(response);
624
+ return this.validateResponse(result.data, validator);
625
+ }
626
+ async parsePaginatedResponse(response, validator) {
627
+ const result = await this.parseEnvelope(response);
628
+ // 对于分页响应,使用元素验证器验证数组中的每个元素
629
+ const data = this.validateArrayResponse(result.data, validator);
630
+ return {
631
+ data,
632
+ pagination: result.pagination,
633
+ };
634
+ }
635
+ async parsePaginatedObjectResponse(response, validator) {
636
+ const result = await this.parseEnvelope(response);
637
+ const data = this.validateResponse(result.data, validator);
638
+ return {
639
+ data,
640
+ pagination: result.pagination,
641
+ };
642
+ }
643
+ async parseResponseKeyResponse(response, responseKey, validator) {
644
+ const result = await this.parseEnvelope(response);
645
+ if (!(responseKey in result)) {
646
+ throw new MeegoError(`[-1] Response missing ${responseKey}`, -1, `Response missing ${responseKey}`, result.err, response.status);
647
+ }
648
+ return this.validateResponse(result[responseKey], validator);
649
+ }
650
+ async parseDownloadResponse(response, options) {
651
+ const contentType = response.headers.get('content-type') || '';
652
+ if (contentType.includes('application/json')) {
653
+ await this.parseEnvelope(response);
654
+ throw new MeegoError('Unexpected JSON response for download', 0, 'Unexpected response');
655
+ }
656
+ if (!response.ok) {
657
+ throw new HttpStatusError(response.status, response.statusText);
658
+ }
659
+ const totalBytes = this.getContentLength(response);
660
+ const progressOptions = options ?? {};
661
+ if (!response.body) {
662
+ const buffer = await response.arrayBuffer();
663
+ if (progressOptions.onProgress || progressOptions.onChunk) {
664
+ const receivedBytes = buffer.byteLength;
665
+ const progress = this.buildDownloadProgress(receivedBytes, totalBytes ?? receivedBytes);
666
+ this.emitDownloadProgress(progressOptions, progress, new Uint8Array(buffer));
667
+ }
668
+ return buffer;
669
+ }
670
+ if (!progressOptions.onProgress && !progressOptions.onChunk) {
671
+ return response.arrayBuffer();
672
+ }
673
+ return this.readStreamToArrayBuffer(response.body, progressOptions, totalBytes);
674
+ }
675
+ async parseDownloadStreamResponse(response, options) {
676
+ const contentType = response.headers.get('content-type') || '';
677
+ if (contentType.includes('application/json')) {
678
+ await this.parseEnvelope(response);
679
+ throw new MeegoError('Unexpected JSON response for download', 0, 'Unexpected response');
680
+ }
681
+ if (!response.ok) {
682
+ throw new HttpStatusError(response.status, response.statusText);
683
+ }
684
+ const totalBytes = this.getContentLength(response);
685
+ const progressOptions = options ?? {};
686
+ if (!response.body) {
687
+ const buffer = new Uint8Array(await response.arrayBuffer());
688
+ if (progressOptions.onProgress || progressOptions.onChunk) {
689
+ const receivedBytes = buffer.byteLength;
690
+ const progress = this.buildDownloadProgress(receivedBytes, totalBytes ?? receivedBytes);
691
+ this.emitDownloadProgress(progressOptions, progress, buffer);
692
+ }
693
+ return new ReadableStream({
694
+ start(controller) {
695
+ controller.enqueue(buffer);
696
+ controller.close();
697
+ },
698
+ });
699
+ }
700
+ return this.wrapStreamWithProgress(response.body, progressOptions, totalBytes);
701
+ }
702
+ getContentLength(response) {
703
+ const headerValue = response.headers.get('content-length');
704
+ if (!headerValue) {
705
+ return undefined;
706
+ }
707
+ const parsed = Number.parseInt(headerValue, 10);
708
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
709
+ }
710
+ buildDownloadProgress(receivedBytes, totalBytes) {
711
+ const progress = typeof totalBytes === 'number' && totalBytes > 0 ? receivedBytes / totalBytes : undefined;
712
+ return { receivedBytes, totalBytes, progress };
713
+ }
714
+ emitDownloadProgress(options, progress, chunk) {
715
+ if (options.onProgress) {
716
+ try {
717
+ options.onProgress(progress);
718
+ }
719
+ catch (error) {
720
+ this.logger.warn('Download progress callback error', {
721
+ callback: 'onProgress',
722
+ error: error instanceof Error ? error.message : 'Unknown error',
723
+ });
724
+ }
725
+ }
726
+ if (chunk && options.onChunk) {
727
+ try {
728
+ options.onChunk(chunk, progress);
729
+ }
730
+ catch (error) {
731
+ this.logger.warn('Download chunk callback error', {
732
+ callback: 'onChunk',
733
+ error: error instanceof Error ? error.message : 'Unknown error',
734
+ });
735
+ }
736
+ }
737
+ }
738
+ async readStreamToArrayBuffer(stream, options, totalBytes) {
739
+ const reader = stream.getReader();
740
+ const chunks = [];
741
+ let receivedBytes = 0;
742
+ let aborted = false;
743
+ const handleAbort = () => {
744
+ aborted = true;
745
+ reader.cancel().catch(() => undefined);
746
+ };
747
+ if (options.signal) {
748
+ if (options.signal.aborted) {
749
+ handleAbort();
750
+ throw new MeegoAbortError();
751
+ }
752
+ options.signal.addEventListener('abort', handleAbort, { once: true });
753
+ }
754
+ try {
755
+ while (true) {
756
+ if (aborted || options.signal?.aborted) {
757
+ throw new MeegoAbortError();
758
+ }
759
+ const { done, value } = await reader.read();
760
+ if (done) {
761
+ break;
762
+ }
763
+ if (!value) {
764
+ continue;
765
+ }
766
+ receivedBytes += value.byteLength;
767
+ const progress = this.buildDownloadProgress(receivedBytes, totalBytes);
768
+ this.emitDownloadProgress(options, progress, value);
769
+ chunks.push(value);
770
+ }
771
+ }
772
+ finally {
773
+ if (options.signal) {
774
+ options.signal.removeEventListener('abort', handleAbort);
775
+ }
776
+ }
777
+ const buffer = new Uint8Array(receivedBytes);
778
+ let offset = 0;
779
+ for (const chunk of chunks) {
780
+ buffer.set(chunk, offset);
781
+ offset += chunk.byteLength;
782
+ }
783
+ return buffer.buffer;
784
+ }
785
+ wrapStreamWithProgress(stream, options, totalBytes) {
786
+ const reader = stream.getReader();
787
+ let receivedBytes = 0;
788
+ let abortHandler;
789
+ const cleanupAbortListener = () => {
790
+ if (options.signal && abortHandler) {
791
+ options.signal.removeEventListener('abort', abortHandler);
792
+ abortHandler = undefined;
793
+ }
794
+ };
795
+ return new ReadableStream({
796
+ start: (controller) => {
797
+ if (!options.signal) {
798
+ return;
799
+ }
800
+ abortHandler = () => {
801
+ cleanupAbortListener();
802
+ reader.cancel().catch(() => undefined);
803
+ controller.error(new MeegoAbortError());
804
+ };
805
+ if (options.signal.aborted) {
806
+ abortHandler();
807
+ }
808
+ else {
809
+ options.signal.addEventListener('abort', abortHandler, { once: true });
810
+ }
811
+ },
812
+ pull: async (controller) => {
813
+ const { done, value } = await reader.read();
814
+ if (done) {
815
+ cleanupAbortListener();
816
+ controller.close();
817
+ return;
818
+ }
819
+ if (!value) {
820
+ return;
821
+ }
822
+ receivedBytes += value.byteLength;
823
+ const progress = this.buildDownloadProgress(receivedBytes, totalBytes);
824
+ this.emitDownloadProgress(options, progress, value);
825
+ controller.enqueue(value);
826
+ },
827
+ cancel: async (reason) => {
828
+ cleanupAbortListener();
829
+ await reader.cancel(reason);
830
+ },
831
+ });
832
+ }
833
+ buildUrlWithResolvedPath(resolvedPath, query) {
834
+ let url = `${this.baseURL}${resolvedPath}`;
835
+ if (query && Object.keys(query).length > 0) {
836
+ const queryString = this.buildQueryString(query);
837
+ url += `?${queryString}`;
838
+ }
839
+ return url;
840
+ }
841
+ }
842
+ //# sourceMappingURL=request.js.map