openclaw-openagent 1.0.13 → 1.0.14

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 (128) hide show
  1. package/dist/src/messaging/executor.d.ts +2 -3
  2. package/dist/src/messaging/executor.js +4 -31
  3. package/dist/src/plugin-ui/assets/openagent-override.js +4 -5
  4. package/dist/src/runtime/update-checker.d.ts +1 -1
  5. package/dist/src/runtime/update-checker.js +9 -46
  6. package/package.json +1 -3
  7. package/src/app/channel-tools.ts +0 -249
  8. package/src/app/discovery-tools.ts +0 -273
  9. package/src/app/download-file-tool.ts +0 -133
  10. package/src/app/hooks.ts +0 -144
  11. package/src/app/index.ts +0 -84
  12. package/src/app/messaging-tools.ts +0 -79
  13. package/src/app/ops-tools.ts +0 -155
  14. package/src/app/remote-agent-tool.ts +0 -762
  15. package/src/app/types.ts +0 -68
  16. package/src/app/upload-file-tool.ts +0 -411
  17. package/src/app/verbose-preflight.ts +0 -190
  18. package/src/auth/config.ts +0 -266
  19. package/src/auth/credential-manager.ts +0 -146
  20. package/src/auth/index.ts +0 -24
  21. package/src/auth/verify.ts +0 -99
  22. package/src/channel.ts +0 -566
  23. package/src/compat.ts +0 -94
  24. package/src/config/config-schema.ts +0 -45
  25. package/src/messaging/aggregator.ts +0 -120
  26. package/src/messaging/collector.ts +0 -89
  27. package/src/messaging/executor.ts +0 -72
  28. package/src/messaging/inbound.ts +0 -150
  29. package/src/messaging/index.ts +0 -11
  30. package/src/messaging/mention-protocol.ts +0 -94
  31. package/src/messaging/process-c2c-request.ts +0 -564
  32. package/src/messaging/process-message.ts +0 -373
  33. package/src/messaging/scheduler.ts +0 -55
  34. package/src/messaging/types.ts +0 -38
  35. package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
  36. package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
  37. package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
  38. package/src/plugin-ui/adapters/oc-unknown.js +0 -48
  39. package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
  40. package/src/plugin-ui/assets/bg.png +0 -0
  41. package/src/plugin-ui/assets/icon.png +0 -0
  42. package/src/plugin-ui/assets/magic.svg +0 -5
  43. package/src/plugin-ui/assets/openagent-override.js +0 -11263
  44. package/src/plugin-ui/build.cjs +0 -463
  45. package/src/plugin-ui/index.ts +0 -18
  46. package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
  47. package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
  48. package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
  49. package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
  50. package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
  51. package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -832
  52. package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
  53. package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
  54. package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
  55. package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
  56. package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
  57. package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
  58. package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -262
  59. package/src/plugin-ui/modules/loader/bootstrap.js +0 -133
  60. package/src/plugin-ui/modules/loader/shared-state.js +0 -838
  61. package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
  62. package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
  63. package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
  64. package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
  65. package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
  66. package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
  67. package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
  68. package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
  69. package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
  70. package/src/plugin-ui/modules/remote-agent/styles.js +0 -903
  71. package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
  72. package/src/plugin-ui/postinstall-deploy.cjs +0 -52
  73. package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
  74. package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
  75. package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
  76. package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
  77. package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
  78. package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
  79. package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
  80. package/src/proxy/auth-proxy.ts +0 -414
  81. package/src/runtime/account.ts +0 -549
  82. package/src/runtime/index.ts +0 -7
  83. package/src/runtime/plugin-runtime.ts +0 -94
  84. package/src/runtime/registry.ts +0 -71
  85. package/src/runtime/update-checker.ts +0 -286
  86. package/src/sdk/CLASS_MAP.md +0 -143
  87. package/src/sdk/index.d.ts +0 -126
  88. package/src/sdk/index.js +0 -23990
  89. package/src/sdk/modules/cloud-search-module.js +0 -1117
  90. package/src/sdk/modules/follow-module.js +0 -1069
  91. package/src/sdk/modules/group-module.js +0 -7397
  92. package/src/sdk/modules/relationship-module.js +0 -2269
  93. package/src/sdk/modules/signaling-module.js +0 -1468
  94. package/src/sdk/modules/tim-upload-plugin.js +0 -730
  95. package/src/sdk/node-env/http-request.js +0 -90
  96. package/src/sdk/node-env/index.js +0 -57
  97. package/src/sdk/node-env/storage.js +0 -114
  98. package/src/sdk/package.json +0 -10
  99. package/src/sdk/tsconfig.json +0 -16
  100. package/src/state/pending-invocation-store.ts +0 -43
  101. package/src/state/store.ts +0 -756
  102. package/src/tim/c2c.ts +0 -451
  103. package/src/tim/channels.ts +0 -364
  104. package/src/tim/client.ts +0 -330
  105. package/src/tim/index.ts +0 -18
  106. package/src/tim/messages.ts +0 -166
  107. package/src/tim/sdk-logger-init.ts +0 -50
  108. package/src/tools.ts +0 -10
  109. package/src/transport/factory.ts +0 -95
  110. package/src/transport/oasn/index.ts +0 -17
  111. package/src/transport/oasn/oasn-agent-card.ts +0 -111
  112. package/src/transport/oasn/oasn-discovery.ts +0 -108
  113. package/src/transport/oasn/oasn-files.ts +0 -210
  114. package/src/transport/oasn/oasn-http.ts +0 -529
  115. package/src/transport/oasn/oasn-invocation.ts +0 -604
  116. package/src/transport/oasn/oasn-normalize.ts +0 -133
  117. package/src/transport/oasn/oasn-register.ts +0 -33
  118. package/src/transport/oasn/oasn-transport.ts +0 -318
  119. package/src/transport/oasn/oasn-types.ts +0 -404
  120. package/src/transport/tim/index.ts +0 -8
  121. package/src/transport/tim/tim-transport.ts +0 -515
  122. package/src/transport/types.ts +0 -538
  123. package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
  124. package/src/types/openclaw.d.ts +0 -97
  125. package/src/types/tencentcloud-chat.d.ts +0 -15
  126. package/src/util/http.ts +0 -113
  127. package/src/util/logger.ts +0 -131
  128. package/src/util/url-resolver.ts +0 -77
@@ -1,529 +0,0 @@
1
- /**
2
- * OASN HTTP 客户端
3
- *
4
- * 设计依据:
5
- * - docs/specs/2026-06-16-oasn-transport-abstraction-design.md §11.5(401 撤销)
6
- * - docs/specs/2026-06-16-oasn-api-module-mapping.md §五(统一错误格式)
7
- *
8
- * 职责:
9
- * 1. 提供 GET/POST/PATCH/multipart 上传/二进制下载 等基础方法
10
- * 2. 注入 `Authorization: Bearer <api_key>` 头
11
- * 3. 解析 OASN 统一错误响应(`{ error: { code, message, retryable, request_id } }`)
12
- * → 抛出 TransportError(保留 retryable / requestId 字段,便于上层判断)
13
- * 4. 401/UNAUTHENTICATED 时触发 `onCredentialRevoked` 回调(§11.5 撤销路径)
14
- * 5. 支持自动重试(仅当 `retryable: true` 时,最多 3 次指数退避)
15
- *
16
- * 不在此层处理:
17
- * - SSE 升级(详见 oasn-invocation.ts 的 subscribeToInvocation)
18
- * - 业务幂等持久化(详见 oasn-invocation.ts + state/store.ts)
19
- * - file_ref LRU(详见 oasn-files.ts)
20
- */
21
-
22
- import { request as httpRequest, type RequestOptions } from 'node:http';
23
- import { request as httpsRequest } from 'node:https';
24
- import { URL } from 'node:url';
25
-
26
- import { logger } from '../../util/logger.js';
27
- import { makeTransportError, type TransportError } from '../types.js';
28
- import type { OasnErrorBody } from './oasn-types.js';
29
-
30
- /** OASN HTTP 客户端配置 */
31
- export interface OasnHttpConfig {
32
- /** API base URL,如 `https://api.oasn.ai` */
33
- apiBase: string;
34
- /** Bearer token(api_key 或 access_token) */
35
- token?: string;
36
- /** 单次请求默认超时(ms) */
37
- defaultTimeoutMs?: number;
38
- /** 自动重试最大次数(仅当响应 retryable=true 时) */
39
- maxRetries?: number;
40
- /**
41
- * 401/UNAUTHENTICATED 触发回调。
42
- * 由 OasnTransport 接入,进一步触发 `credential_expired` 事件 + 清本地凭证(§11.5)。
43
- */
44
- onCredentialRevoked?: () => void;
45
- }
46
-
47
- /** 请求选项 */
48
- export interface OasnRequestOptions {
49
- /** 自定义超时(覆盖 defaultTimeoutMs) */
50
- timeoutMs?: number;
51
- /** 自定义额外头(Authorization 由 client 自动注入,无需传入) */
52
- headers?: Record<string, string>;
53
- /** 跳过自动重试(如轮询接口自己控制节奏) */
54
- skipRetry?: boolean;
55
- /** Abort 信号 */
56
- signal?: AbortSignal;
57
- }
58
-
59
- const DEFAULT_TIMEOUT_MS = 30_000;
60
- const DEFAULT_MAX_RETRIES = 3;
61
-
62
- type GlobalAgentState = {
63
- NO_PROXY?: string | null;
64
- };
65
-
66
- type GlobalWithAgent = typeof globalThis & {
67
- GLOBAL_AGENT?: GlobalAgentState;
68
- };
69
-
70
- /**
71
- * OpenClaw gateway may install global-agent for model/provider traffic. OASN
72
- * REL endpoints should keep their original TLS hostname; otherwise a local
73
- * proxy can turn `https://oasn-test...` into a certificate check against
74
- * `localhost`.
75
- */
76
- export function ensureOasnHostBypassesGlobalAgentProxy(hostname: string): void {
77
- const normalized = hostname.trim().toLowerCase();
78
- if (!normalized || normalized === 'localhost' || normalized === '127.0.0.1' || normalized === '::1') return;
79
-
80
- const globalAgent = (globalThis as GlobalWithAgent).GLOBAL_AGENT;
81
- if (!globalAgent || typeof globalAgent !== 'object') return;
82
-
83
- const existing = String(globalAgent.NO_PROXY || '');
84
- const entries = existing.split(/[\s,]+/).filter(Boolean);
85
- if (entries.some((entry) => entry.toLowerCase() === normalized)) return;
86
-
87
- globalAgent.NO_PROXY = entries.length > 0 ? `${entries.join(',')},${normalized}` : normalized;
88
- }
89
-
90
- function hasHeader(headers: Record<string, string>, name: string): boolean {
91
- const lower = name.toLowerCase();
92
- return Object.keys(headers).some((key) => key.toLowerCase() === lower);
93
- }
94
-
95
- export function buildNodeRequestOptions(
96
- url: URL,
97
- method: string,
98
- headers: Record<string, string>,
99
- ): RequestOptions & { servername?: string } {
100
- const isHttps = url.protocol === 'https:';
101
- const requestHeaders = { ...headers };
102
- if (!hasHeader(requestHeaders, 'Host')) {
103
- requestHeaders.Host = url.host;
104
- }
105
- const options: RequestOptions & { servername?: string } = {
106
- method,
107
- headers: requestHeaders,
108
- hostname: url.hostname,
109
- port: url.port || (isHttps ? 443 : 80),
110
- path: `${url.pathname}${url.search}`,
111
- };
112
- if (isHttps) {
113
- options.servername = url.hostname;
114
- }
115
- return options;
116
- }
117
-
118
- // ───────────────────────────────────────────────────────────────
119
- // 错误转换
120
- // ───────────────────────────────────────────────────────────────
121
-
122
- /**
123
- * 把 HTTP 响应(含可能的 OASN 统一错误体)转换为 TransportError。
124
- * 如果响应非 OASN 统一格式,使用 HTTP 状态码做兜底。
125
- */
126
- function toTransportError(
127
- statusCode: number,
128
- rawBody: string,
129
- ): TransportError {
130
- let code = `HTTP_${statusCode}`;
131
- let message = rawBody.slice(0, 500);
132
- let retryable = statusCode >= 500 || statusCode === 429 || statusCode === 408;
133
- let requestId: string | undefined;
134
-
135
- try {
136
- const parsed = JSON.parse(rawBody) as Partial<OasnErrorBody>;
137
- if (parsed?.error?.code) {
138
- code = parsed.error.code;
139
- message = parsed.error.message ?? message;
140
- retryable = parsed.error.retryable ?? retryable;
141
- requestId = parsed.error.request_id;
142
- }
143
- } catch {
144
- // 非 JSON 响应,保留兜底值
145
- }
146
-
147
- return makeTransportError(code, `[${statusCode}] ${message}`, { retryable, requestId });
148
- }
149
-
150
- // ───────────────────────────────────────────────────────────────
151
- // OasnHttpClient
152
- // ───────────────────────────────────────────────────────────────
153
-
154
- export class OasnHttpClient {
155
- private _config: OasnHttpConfig;
156
-
157
- constructor(config: OasnHttpConfig) {
158
- if (!config.apiBase) {
159
- throw new Error('OasnHttpClient: apiBase is required');
160
- }
161
- this._config = {
162
- apiBase: config.apiBase,
163
- token: config.token,
164
- defaultTimeoutMs: config.defaultTimeoutMs ?? DEFAULT_TIMEOUT_MS,
165
- maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
166
- onCredentialRevoked: config.onCredentialRevoked,
167
- };
168
- }
169
-
170
- /** 动态更新 token —— 注册成功后由 OasnTransport 调用 */
171
- setToken(token: string | undefined): void {
172
- this._config.token = token;
173
- }
174
-
175
- // ─────────────────────────────────────────────────────────────
176
- // 公共方法
177
- // ─────────────────────────────────────────────────────────────
178
-
179
- async get<T>(path: string, options?: OasnRequestOptions): Promise<T> {
180
- return this._jsonRequest<T>('GET', path, undefined, options);
181
- }
182
-
183
- async post<T>(path: string, body: unknown, options?: OasnRequestOptions): Promise<T> {
184
- return this._jsonRequest<T>('POST', path, body, options);
185
- }
186
-
187
- async patch<T>(path: string, body: unknown, options?: OasnRequestOptions): Promise<T> {
188
- return this._jsonRequest<T>('PATCH', path, body, options);
189
- }
190
-
191
- /**
192
- * multipart/form-data 上传。
193
- * 仅支持 file + metadata 字段,不做通用 multipart 支持(OASN 上传场景固定)。
194
- */
195
- async postMultipart<T>(
196
- path: string,
197
- parts: {
198
- file: { content: Buffer; filename: string; mimeType?: string };
199
- fields?: Record<string, string>;
200
- },
201
- options?: OasnRequestOptions,
202
- ): Promise<T> {
203
- const boundary = `------oasn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
204
- const chunks: Buffer[] = [];
205
-
206
- // 普通字段
207
- if (parts.fields) {
208
- for (const [key, value] of Object.entries(parts.fields)) {
209
- chunks.push(Buffer.from(
210
- `--${boundary}\r\n`
211
- + `Content-Disposition: form-data; name="${key}"\r\n\r\n`
212
- + `${value}\r\n`,
213
- ));
214
- }
215
- }
216
-
217
- // 文件字段
218
- const mime = parts.file.mimeType ?? 'application/octet-stream';
219
- chunks.push(Buffer.from(
220
- `--${boundary}\r\n`
221
- + `Content-Disposition: form-data; name="file"; filename="${parts.file.filename}"\r\n`
222
- + `Content-Type: ${mime}\r\n\r\n`,
223
- ));
224
- chunks.push(parts.file.content);
225
- chunks.push(Buffer.from(`\r\n--${boundary}--\r\n`));
226
-
227
- const body = Buffer.concat(chunks);
228
-
229
- return this._rawRequest<T>('POST', path, body, {
230
- ...options,
231
- headers: {
232
- 'Content-Type': `multipart/form-data; boundary=${boundary}`,
233
- ...options?.headers,
234
- },
235
- isJsonBody: false,
236
- expectJsonResponse: true,
237
- });
238
- }
239
-
240
- /**
241
- * 下载二进制内容(用于 Artifact 下载)。
242
- * 不解析响应,直接返回 ArrayBuffer + 头信息。
243
- */
244
- async getBinary(
245
- path: string,
246
- options?: OasnRequestOptions,
247
- ): Promise<{ content: ArrayBuffer; mimeType: string; displayName: string }> {
248
- const url = this._resolveUrl(path);
249
- const headers = this._buildHeaders({});
250
- const timeoutMs = options?.timeoutMs ?? this._config.defaultTimeoutMs!;
251
- return this._doBinaryRequest(url, headers, timeoutMs, options?.signal);
252
- }
253
-
254
- /**
255
- * 暴露底层 raw 请求,用于 SSE 等流式场景(如 oasn-invocation 的 SSE 升级)。
256
- * 调用方自行处理流式响应。
257
- */
258
- rawHttpRequest(
259
- method: string,
260
- path: string,
261
- headers: Record<string, string>,
262
- body?: Buffer | string,
263
- options?: OasnRequestOptions,
264
- ): Promise<{ statusCode: number; headers: Record<string, string>; body: string }> {
265
- const url = this._resolveUrl(path);
266
- const fullHeaders = this._buildHeaders(headers);
267
- const timeoutMs = options?.timeoutMs ?? this._config.defaultTimeoutMs!;
268
- return this._doRawRequest(url, method, fullHeaders, body, timeoutMs, options?.signal);
269
- }
270
-
271
- // ─────────────────────────────────────────────────────────────
272
- // 内部实现
273
- // ─────────────────────────────────────────────────────────────
274
-
275
- private _resolveUrl(path: string): URL {
276
- if (/^https?:\/\//i.test(path)) {
277
- return new URL(path);
278
- }
279
- // path 必须以 / 开头
280
- const base = this._config.apiBase.replace(/\/$/, '');
281
- return new URL(`${base}${path}`);
282
- }
283
-
284
- private _buildHeaders(extra: Record<string, string>): Record<string, string> {
285
- const headers: Record<string, string> = {
286
- Accept: 'application/json',
287
- ...extra,
288
- };
289
- if (this._config.token) {
290
- headers['Authorization'] = `Bearer ${this._config.token}`;
291
- }
292
- return headers;
293
- }
294
-
295
- /**
296
- * JSON 请求 —— body 自动 JSON.stringify,响应 JSON.parse。
297
- * 含重试逻辑(仅当 retryable=true 时)。
298
- */
299
- private async _jsonRequest<T>(
300
- method: string,
301
- path: string,
302
- body: unknown,
303
- options?: OasnRequestOptions,
304
- ): Promise<T> {
305
- return this._rawRequest<T>(method, path, body, {
306
- ...options,
307
- isJsonBody: true,
308
- expectJsonResponse: true,
309
- });
310
- }
311
-
312
- private async _rawRequest<T>(
313
- method: string,
314
- path: string,
315
- body: unknown,
316
- options: OasnRequestOptions & { isJsonBody: boolean; expectJsonResponse: boolean },
317
- ): Promise<T> {
318
- const url = this._resolveUrl(path);
319
- const headers = this._buildHeaders(options?.headers ?? {});
320
-
321
- let bodyBuffer: Buffer | undefined;
322
- if (body !== undefined) {
323
- if (options.isJsonBody) {
324
- const json = typeof body === 'string' ? body : JSON.stringify(body);
325
- bodyBuffer = Buffer.from(json);
326
- headers['Content-Type'] = 'application/json';
327
- headers['Content-Length'] = String(bodyBuffer.length);
328
- } else {
329
- bodyBuffer = body as Buffer;
330
- if (!headers['Content-Length']) {
331
- headers['Content-Length'] = String(bodyBuffer.length);
332
- }
333
- }
334
- }
335
-
336
- const maxAttempts = options.skipRetry ? 1 : (this._config.maxRetries! + 1);
337
- let lastErr: unknown;
338
-
339
- for (let attempt = 0; attempt < maxAttempts; attempt++) {
340
- try {
341
- if (attempt > 0) {
342
- // 指数退避:200ms / 400ms / 800ms(含少量随机抖动)
343
- const delay = 200 * Math.pow(2, attempt - 1) + Math.floor(Math.random() * 50);
344
- await new Promise((r) => setTimeout(r, delay));
345
- logger.info(`[transport/oasn/http] retry ${attempt}/${maxAttempts - 1} ${method} ${path}`);
346
- }
347
-
348
- const { statusCode, body: respBody } = await this._doRawRequest(
349
- url,
350
- method,
351
- headers,
352
- bodyBuffer,
353
- options?.timeoutMs ?? this._config.defaultTimeoutMs!,
354
- options?.signal,
355
- );
356
-
357
- if (statusCode >= 200 && statusCode < 300) {
358
- if (!options.expectJsonResponse) return undefined as T;
359
- if (statusCode === 204 || !respBody) return undefined as T;
360
- try {
361
- return JSON.parse(respBody) as T;
362
- } catch (parseErr) {
363
- throw makeTransportError(
364
- 'INVALID_JSON_RESPONSE',
365
- `Failed to parse OASN response: ${(parseErr as Error).message}`,
366
- );
367
- }
368
- }
369
-
370
- // 错误响应
371
- const err = toTransportError(statusCode, respBody);
372
-
373
- // §11.5 401/UNAUTHENTICATED 撤销路径
374
- if (statusCode === 401 || err.code === 'UNAUTHENTICATED') {
375
- logger.warn(`[transport/oasn/http] 401 UNAUTHENTICATED on ${method} ${path}`);
376
- this._config.onCredentialRevoked?.();
377
- throw err; // 401 不重试
378
- }
379
-
380
- if (!err.retryable || attempt === maxAttempts - 1) throw err;
381
- lastErr = err;
382
- logger.warn(`[transport/oasn/http] retryable error code=${err.code} on ${method} ${path}, will retry`);
383
- } catch (err) {
384
- // 网络层错误(DNS / 连接 / 超时) —— 视为可重试
385
- const isTransport = (err as TransportError).code !== undefined;
386
- if (isTransport) throw err;
387
- lastErr = err;
388
- if (attempt === maxAttempts - 1) {
389
- throw makeTransportError(
390
- 'NETWORK_ERROR',
391
- `${method} ${path} failed: ${(err as Error).message}`,
392
- { retryable: true },
393
- );
394
- }
395
- }
396
- }
397
-
398
- // 不可达,但 TS 需要
399
- throw lastErr ?? new Error('Unknown HTTP error');
400
- }
401
-
402
- /** 执行单次 HTTP 请求(不解析响应体格式) */
403
- private _doRawRequest(
404
- url: URL,
405
- method: string,
406
- headers: Record<string, string>,
407
- body: Buffer | string | undefined,
408
- timeoutMs: number,
409
- signal?: AbortSignal,
410
- ): Promise<{ statusCode: number; headers: Record<string, string>; body: string }> {
411
- const isHttps = url.protocol === 'https:';
412
- const requestFn = isHttps ? httpsRequest : httpRequest;
413
- ensureOasnHostBypassesGlobalAgentProxy(url.hostname);
414
-
415
- return new Promise((resolve, reject) => {
416
- const reqOptions = buildNodeRequestOptions(url, method, headers);
417
-
418
- let settled = false;
419
- let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
420
- const finish = (fn: () => void) => {
421
- if (settled) return;
422
- settled = true;
423
- if (timeoutHandle) clearTimeout(timeoutHandle);
424
- fn();
425
- };
426
-
427
- const req = requestFn(reqOptions, (res) => {
428
- const chunks: Buffer[] = [];
429
- res.on('data', (chunk: Buffer) => chunks.push(chunk));
430
- res.on('end', () => {
431
- const respBody = Buffer.concat(chunks).toString('utf-8');
432
- const respHeaders: Record<string, string> = {};
433
- for (const [k, v] of Object.entries(res.headers)) {
434
- if (typeof v === 'string') respHeaders[k] = v;
435
- else if (Array.isArray(v)) respHeaders[k] = v.join(', ');
436
- }
437
- finish(() => resolve({
438
- statusCode: res.statusCode ?? 0,
439
- headers: respHeaders,
440
- body: respBody,
441
- }));
442
- });
443
- res.on('error', (err) => finish(() => reject(err)));
444
- });
445
-
446
- req.on('error', (err) => finish(() => reject(err)));
447
-
448
- timeoutHandle = setTimeout(() => {
449
- req.destroy(new Error(`OASN HTTP timeout after ${timeoutMs}ms`));
450
- }, timeoutMs);
451
-
452
- // Abort 支持
453
- if (signal) {
454
- const onAbort = () => {
455
- req.destroy(new Error('Request aborted'));
456
- };
457
- if (signal.aborted) onAbort();
458
- else signal.addEventListener('abort', onAbort, { once: true });
459
- }
460
-
461
- if (body) req.write(body);
462
- req.end();
463
- });
464
- }
465
-
466
- /** 二进制下载 */
467
- private _doBinaryRequest(
468
- url: URL,
469
- headers: Record<string, string>,
470
- timeoutMs: number,
471
- signal?: AbortSignal,
472
- ): Promise<{ content: ArrayBuffer; mimeType: string; displayName: string }> {
473
- const isHttps = url.protocol === 'https:';
474
- const requestFn = isHttps ? httpsRequest : httpRequest;
475
- ensureOasnHostBypassesGlobalAgentProxy(url.hostname);
476
-
477
- return new Promise((resolve, reject) => {
478
- const reqOptions = buildNodeRequestOptions(url, 'GET', headers);
479
-
480
- let settled = false;
481
- let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
482
- const finish = (fn: () => void) => {
483
- if (settled) return;
484
- settled = true;
485
- if (timeoutHandle) clearTimeout(timeoutHandle);
486
- fn();
487
- };
488
-
489
- const req = requestFn(reqOptions, (res) => {
490
- const status = res.statusCode ?? 0;
491
- const chunks: Buffer[] = [];
492
- res.on('data', (chunk: Buffer) => chunks.push(chunk));
493
- res.on('end', () => {
494
- const buf = Buffer.concat(chunks);
495
- if (status >= 200 && status < 300) {
496
- const mime = String(res.headers['content-type'] ?? 'application/octet-stream');
497
- const disposition = String(res.headers['content-disposition'] ?? '');
498
- const nameMatch = disposition.match(/filename\*?=([^;]+)/i);
499
- const displayName = nameMatch
500
- ? decodeURIComponent(nameMatch[1].replace(/^UTF-8''/i, '').replace(/^"|"$/g, ''))
501
- : 'artifact';
502
- finish(() => resolve({
503
- content: buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength) as ArrayBuffer,
504
- mimeType: mime,
505
- displayName,
506
- }));
507
- } else {
508
- finish(() => reject(toTransportError(status, buf.toString('utf-8'))));
509
- }
510
- });
511
- res.on('error', (err) => finish(() => reject(err)));
512
- });
513
-
514
- req.on('error', (err) => finish(() => reject(err)));
515
-
516
- timeoutHandle = setTimeout(() => {
517
- req.destroy(new Error(`OASN binary download timeout after ${timeoutMs}ms`));
518
- }, timeoutMs);
519
-
520
- if (signal) {
521
- const onAbort = () => req.destroy(new Error('Request aborted'));
522
- if (signal.aborted) onAbort();
523
- else signal.addEventListener('abort', onAbort, { once: true });
524
- }
525
-
526
- req.end();
527
- });
528
- }
529
- }