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.
- package/dist/src/messaging/executor.d.ts +2 -3
- package/dist/src/messaging/executor.js +4 -31
- package/dist/src/plugin-ui/assets/openagent-override.js +4 -5
- package/dist/src/runtime/update-checker.d.ts +1 -1
- package/dist/src/runtime/update-checker.js +9 -46
- package/package.json +1 -3
- package/src/app/channel-tools.ts +0 -249
- package/src/app/discovery-tools.ts +0 -273
- package/src/app/download-file-tool.ts +0 -133
- package/src/app/hooks.ts +0 -144
- package/src/app/index.ts +0 -84
- package/src/app/messaging-tools.ts +0 -79
- package/src/app/ops-tools.ts +0 -155
- package/src/app/remote-agent-tool.ts +0 -762
- package/src/app/types.ts +0 -68
- package/src/app/upload-file-tool.ts +0 -411
- package/src/app/verbose-preflight.ts +0 -190
- package/src/auth/config.ts +0 -266
- package/src/auth/credential-manager.ts +0 -146
- package/src/auth/index.ts +0 -24
- package/src/auth/verify.ts +0 -99
- package/src/channel.ts +0 -566
- package/src/compat.ts +0 -94
- package/src/config/config-schema.ts +0 -45
- package/src/messaging/aggregator.ts +0 -120
- package/src/messaging/collector.ts +0 -89
- package/src/messaging/executor.ts +0 -72
- package/src/messaging/inbound.ts +0 -150
- package/src/messaging/index.ts +0 -11
- package/src/messaging/mention-protocol.ts +0 -94
- package/src/messaging/process-c2c-request.ts +0 -564
- package/src/messaging/process-message.ts +0 -373
- package/src/messaging/scheduler.ts +0 -55
- package/src/messaging/types.ts +0 -38
- package/src/plugin-ui/adapters/oc-2026-04.js +0 -103
- package/src/plugin-ui/adapters/oc-2026-05.js +0 -125
- package/src/plugin-ui/adapters/oc-2026-06.js +0 -125
- package/src/plugin-ui/adapters/oc-unknown.js +0 -48
- package/src/plugin-ui/assets/agentbook-icon.svg +0 -5
- package/src/plugin-ui/assets/bg.png +0 -0
- package/src/plugin-ui/assets/icon.png +0 -0
- package/src/plugin-ui/assets/magic.svg +0 -5
- package/src/plugin-ui/assets/openagent-override.js +0 -11263
- package/src/plugin-ui/build.cjs +0 -463
- package/src/plugin-ui/index.ts +0 -18
- package/src/plugin-ui/modules/agent-book/panel/agent-book.js +0 -570
- package/src/plugin-ui/modules/agent-book/panel/agent-card.js +0 -170
- package/src/plugin-ui/modules/agent-book/panel/agent-data.js +0 -643
- package/src/plugin-ui/modules/agent-book/panel/inject-ui.js +0 -665
- package/src/plugin-ui/modules/agent-book/panel/mention-state.js +0 -206
- package/src/plugin-ui/modules/agent-book/panel/styles.js +0 -832
- package/src/plugin-ui/modules/agent-book/panel/theme-adapter.js +0 -86
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/components-core.js +0 -295
- package/src/plugin-ui/modules/agent-book/remote-agent-tool/thought-chain-card.js +0 -211
- package/src/plugin-ui/modules/agent-book/scanner.js +0 -131
- package/src/plugin-ui/modules/agent-book/travelcard/travel-cards.js +0 -500
- package/src/plugin-ui/modules/agent-book/travelcard/travel-engine.js +0 -652
- package/src/plugin-ui/modules/agent-book/travelcard/travel-styles.js +0 -262
- package/src/plugin-ui/modules/loader/bootstrap.js +0 -133
- package/src/plugin-ui/modules/loader/shared-state.js +0 -838
- package/src/plugin-ui/modules/loader/ws-intercept.js +0 -199
- package/src/plugin-ui/modules/remote-agent/chunk-parser.js +0 -161
- package/src/plugin-ui/modules/remote-agent/execution-card.js +0 -530
- package/src/plugin-ui/modules/remote-agent/markdown-renderer.js +0 -256
- package/src/plugin-ui/modules/remote-agent/media-links.js +0 -151
- package/src/plugin-ui/modules/remote-agent/native-style-adapter.js +0 -134
- package/src/plugin-ui/modules/remote-agent/output-card.js +0 -342
- package/src/plugin-ui/modules/remote-agent/progress-store.js +0 -363
- package/src/plugin-ui/modules/remote-agent/render-hooks.js +0 -1700
- package/src/plugin-ui/modules/remote-agent/styles.js +0 -903
- package/src/plugin-ui/modules/remote-agent/tool-card-model.js +0 -130
- package/src/plugin-ui/postinstall-deploy.cjs +0 -52
- package/src/plugin-ui/ui-extension-loader/backup.ts +0 -92
- package/src/plugin-ui/ui-extension-loader/index.ts +0 -276
- package/src/plugin-ui/ui-extension-loader/locator.ts +0 -152
- package/src/plugin-ui/ui-extension-loader/manifest.ts +0 -57
- package/src/plugin-ui/ui-extension-loader/registry-regex.ts +0 -944
- package/src/plugin-ui/ui-extension-loader/removed-extensions.ts +0 -70
- package/src/plugin-ui/ui-extension-loader/types.ts +0 -72
- package/src/proxy/auth-proxy.ts +0 -414
- package/src/runtime/account.ts +0 -549
- package/src/runtime/index.ts +0 -7
- package/src/runtime/plugin-runtime.ts +0 -94
- package/src/runtime/registry.ts +0 -71
- package/src/runtime/update-checker.ts +0 -286
- package/src/sdk/CLASS_MAP.md +0 -143
- package/src/sdk/index.d.ts +0 -126
- package/src/sdk/index.js +0 -23990
- package/src/sdk/modules/cloud-search-module.js +0 -1117
- package/src/sdk/modules/follow-module.js +0 -1069
- package/src/sdk/modules/group-module.js +0 -7397
- package/src/sdk/modules/relationship-module.js +0 -2269
- package/src/sdk/modules/signaling-module.js +0 -1468
- package/src/sdk/modules/tim-upload-plugin.js +0 -730
- package/src/sdk/node-env/http-request.js +0 -90
- package/src/sdk/node-env/index.js +0 -57
- package/src/sdk/node-env/storage.js +0 -114
- package/src/sdk/package.json +0 -10
- package/src/sdk/tsconfig.json +0 -16
- package/src/state/pending-invocation-store.ts +0 -43
- package/src/state/store.ts +0 -756
- package/src/tim/c2c.ts +0 -451
- package/src/tim/channels.ts +0 -364
- package/src/tim/client.ts +0 -330
- package/src/tim/index.ts +0 -18
- package/src/tim/messages.ts +0 -166
- package/src/tim/sdk-logger-init.ts +0 -50
- package/src/tools.ts +0 -10
- package/src/transport/factory.ts +0 -95
- package/src/transport/oasn/index.ts +0 -17
- package/src/transport/oasn/oasn-agent-card.ts +0 -111
- package/src/transport/oasn/oasn-discovery.ts +0 -108
- package/src/transport/oasn/oasn-files.ts +0 -210
- package/src/transport/oasn/oasn-http.ts +0 -529
- package/src/transport/oasn/oasn-invocation.ts +0 -604
- package/src/transport/oasn/oasn-normalize.ts +0 -133
- package/src/transport/oasn/oasn-register.ts +0 -33
- package/src/transport/oasn/oasn-transport.ts +0 -318
- package/src/transport/oasn/oasn-types.ts +0 -404
- package/src/transport/tim/index.ts +0 -8
- package/src/transport/tim/tim-transport.ts +0 -515
- package/src/transport/types.ts +0 -538
- package/src/types/openclaw-plugin-sdk-media-store.d.ts +0 -9
- package/src/types/openclaw.d.ts +0 -97
- package/src/types/tencentcloud-chat.d.ts +0 -15
- package/src/util/http.ts +0 -113
- package/src/util/logger.ts +0 -131
- 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
|
-
}
|