@larktask/aamp-feishu-task-agent 0.1.0-dev.171

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.
@@ -0,0 +1,326 @@
1
+ import dns from 'node:dns';
2
+ import os from 'node:os';
3
+
4
+ const PROXY_ENV_KEYS = [
5
+ 'HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy',
6
+ 'ALL_PROXY', 'all_proxy', 'NO_PROXY', 'no_proxy',
7
+ 'NODE_USE_ENV_PROXY',
8
+ ];
9
+
10
+ const DNS_CODES = new Set([
11
+ 'ENOTFOUND', 'EAI_AGAIN', 'ENODATA', 'ESERVFAIL', 'ERR_DNS_SET_SERVERS_FAILED',
12
+ ]);
13
+ const CONNECT_TIMEOUT_CODES = new Set(['UND_ERR_CONNECT_TIMEOUT']);
14
+ const TIMEOUT_CODES = new Set([
15
+ 'ETIMEDOUT', 'ESOCKETTIMEDOUT', 'UND_ERR_HEADERS_TIMEOUT', 'UND_ERR_BODY_TIMEOUT', 'ABORT_ERR',
16
+ ]);
17
+ const RESET_CODES = new Set(['ECONNRESET', 'EPIPE', 'ENETRESET', 'UND_ERR_SOCKET']);
18
+ const UNREACHABLE_CODES = new Set(['ECONNREFUSED', 'ENETUNREACH', 'EHOSTUNREACH', 'ENETDOWN']);
19
+ const TLS_CODES = new Set([
20
+ 'CERT_HAS_EXPIRED',
21
+ 'DEPTH_ZERO_SELF_SIGNED_CERT',
22
+ 'ERR_TLS_CERT_ALTNAME_INVALID',
23
+ 'SELF_SIGNED_CERT_IN_CHAIN',
24
+ 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY',
25
+ 'UNABLE_TO_VERIFY_LEAF_SIGNATURE',
26
+ ]);
27
+
28
+ function asErrorDetails(error) {
29
+ return error && typeof error === 'object' ? error : {};
30
+ }
31
+
32
+ function collectErrorChain(error) {
33
+ const chain = [];
34
+ const seen = new Set();
35
+ let current = error;
36
+ while (current !== undefined && current !== null && !seen.has(current)) {
37
+ chain.push(current);
38
+ if (typeof current !== 'object') break;
39
+ seen.add(current);
40
+ current = current.cause;
41
+ }
42
+ return chain;
43
+ }
44
+
45
+ function errorCodes(error) {
46
+ return collectErrorChain(error)
47
+ .map((item) => asErrorDetails(item).code)
48
+ .filter((value) => typeof value === 'string')
49
+ .map((value) => value.toUpperCase());
50
+ }
51
+
52
+ function errorStatus(error) {
53
+ for (const item of collectErrorChain(error)) {
54
+ const details = asErrorDetails(item);
55
+ const value = Number(details.status ?? details.statusCode ?? details.response?.status);
56
+ if (Number.isInteger(value) && value >= 100 && value <= 599) return value;
57
+ }
58
+ const text = collectErrorChain(error)
59
+ .map((item) => item instanceof Error ? item.message : String(item))
60
+ .join(' | ');
61
+ const match = text.match(/(?:\bHTTP(?:\/\d(?:\.\d)?)?\s+|\bstatus(?:Code)?\s*[=:]\s*)([1-5]\d{2})\b/i);
62
+ if (match) return Number(match[1]);
63
+ return undefined;
64
+ }
65
+
66
+ function describeSingleError(error) {
67
+ if (!(error instanceof Error)) return String(error);
68
+ const details = asErrorDetails(error);
69
+ const parts = [error.message || error.name];
70
+ for (const [key, value] of [
71
+ ['code', details.code],
72
+ ['errno', details.errno],
73
+ ['syscall', details.syscall],
74
+ ['hostname', details.hostname],
75
+ ['host', details.host],
76
+ ['address', details.address],
77
+ ['port', details.port],
78
+ ['status', details.status ?? details.statusCode ?? details.response?.status],
79
+ ]) {
80
+ if (value !== undefined && value !== null && value !== '') parts.push(`${key}=${value}`);
81
+ }
82
+ return parts.join(' | ');
83
+ }
84
+
85
+ export function describeNetworkError(error) {
86
+ const chain = collectErrorChain(error);
87
+ if (!chain.length) return String(error);
88
+ return chain
89
+ .map((item, index) => `${index === 0 ? '' : 'cause='}${describeSingleError(item)}`)
90
+ .join(' | ');
91
+ }
92
+
93
+ export function classifyNetworkError(error) {
94
+ const status = errorStatus(error);
95
+ if (status !== undefined) return status >= 500 ? 'http_5xx' : 'http_4xx';
96
+
97
+ const codes = errorCodes(error);
98
+ if (codes.some((code) => DNS_CODES.has(code))) return 'dns';
99
+ if (codes.some((code) => CONNECT_TIMEOUT_CODES.has(code))) return 'connect_timeout';
100
+ if (codes.some((code) => TIMEOUT_CODES.has(code))) return 'timeout';
101
+ if (codes.some((code) => RESET_CODES.has(code))) return 'connection_reset';
102
+ if (codes.some((code) => UNREACHABLE_CODES.has(code))) return 'unreachable';
103
+ if (codes.some((code) => TLS_CODES.has(code) || code.startsWith('ERR_TLS_'))) return 'tls';
104
+
105
+ const message = describeNetworkError(error).toLowerCase();
106
+ const includesCode = (knownCodes) => [...knownCodes].some((code) => message.includes(code.toLowerCase()));
107
+ if (includesCode(DNS_CODES) || message.includes('getaddrinfo') || message.includes('dns')) return 'dns';
108
+ if (includesCode(CONNECT_TIMEOUT_CODES) || message.includes('connect timeout')) return 'connect_timeout';
109
+ if (includesCode(TIMEOUT_CODES) || message.includes('timed out') || message.includes('timeout') || message.includes('aborted')) return 'timeout';
110
+ if (includesCode(RESET_CODES) || message.includes('connection reset') || message.includes('socket closed')) return 'connection_reset';
111
+ if (includesCode(UNREACHABLE_CODES)) return 'unreachable';
112
+ if (includesCode(TLS_CODES) || message.includes('certificate') || message.includes('tls')) return 'tls';
113
+ if (message.includes('fetch failed') || message.includes('network error')) return 'network';
114
+ return 'unknown';
115
+ }
116
+
117
+ export function isRetryableHttpStatus(status) {
118
+ return status === 408 || status === 409 || status === 425 || status === 429 || status >= 500;
119
+ }
120
+
121
+ export function isRetryableNetworkError(error) {
122
+ const status = errorStatus(error);
123
+ if (status !== undefined) return isRetryableHttpStatus(status);
124
+ return ['dns', 'connect_timeout', 'timeout', 'connection_reset', 'unreachable', 'network']
125
+ .includes(classifyNetworkError(error));
126
+ }
127
+
128
+ function defaultSleep(ms) {
129
+ if (ms <= 0) return Promise.resolve();
130
+ return new Promise((resolve) => setTimeout(resolve, ms));
131
+ }
132
+
133
+ export async function withNetworkRetry(operation, options = {}) {
134
+ const maxAttempts = Math.max(1, Number(options.maxAttempts) || 1);
135
+ const baseDelayMs = Math.max(0, Number(options.baseDelayMs) || 0);
136
+ const shouldRetry = options.shouldRetry || isRetryableNetworkError;
137
+ const sleep = options.sleep || defaultSleep;
138
+ let lastError;
139
+
140
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
141
+ try {
142
+ return await operation({ attempt, maxAttempts });
143
+ } catch (error) {
144
+ lastError = error;
145
+ if (attempt >= maxAttempts || !shouldRetry(error)) throw error;
146
+ const nextDelayMs = baseDelayMs * 2 ** (attempt - 1);
147
+ await options.onRetry?.({
148
+ attempt,
149
+ maxAttempts,
150
+ nextDelayMs,
151
+ category: classifyNetworkError(error),
152
+ error: describeNetworkError(error),
153
+ });
154
+ await sleep(nextDelayMs);
155
+ }
156
+ }
157
+ throw lastError;
158
+ }
159
+
160
+ export function launchDetachedDiagnostic(operation, onError) {
161
+ void Promise.resolve()
162
+ .then(operation)
163
+ .catch(async (error) => {
164
+ try {
165
+ await onError?.(error);
166
+ } catch {
167
+ // Diagnostics must never affect the real bridge operation.
168
+ }
169
+ });
170
+ }
171
+
172
+ export function agentStartRetryError(events, expectedAgentNames, attempt, maxAttempts) {
173
+ if (Number(attempt) >= Number(maxAttempts)) return undefined;
174
+ const expected = new Set(expectedAgentNames || []);
175
+ for (const event of events || []) {
176
+ if (event?.type !== 'agent.failed' || !expected.has(event.agent)) continue;
177
+ const error = new Error(String(event.message || `${event.agent} Agent Bridge 启动失败`));
178
+ if (isRetryableNetworkError(error)) return error;
179
+ }
180
+ return undefined;
181
+ }
182
+
183
+ export function safeDiagnosticUrl(rawUrl) {
184
+ const parsed = new URL(rawUrl);
185
+ parsed.username = '';
186
+ parsed.password = '';
187
+ parsed.search = '';
188
+ parsed.hash = '';
189
+ return parsed.toString();
190
+ }
191
+
192
+ export function networkEnvironmentSummary(environment = process.env) {
193
+ return {
194
+ node: process.version,
195
+ platform: process.platform,
196
+ arch: process.arch,
197
+ osRelease: os.release(),
198
+ proxyEnvPresent: PROXY_ENV_KEYS.filter((key) => Boolean(environment[key])),
199
+ };
200
+ }
201
+
202
+ function normalizedDnsResults(records) {
203
+ const list = Array.isArray(records) ? records : [records];
204
+ return list
205
+ .filter((record) => record && typeof record.address === 'string')
206
+ .map((record) => ({ address: record.address, family: Number(record.family) || record.family }));
207
+ }
208
+
209
+ function fetchTimeoutSignal(timeoutMs) {
210
+ if (typeof AbortSignal?.timeout === 'function') return AbortSignal.timeout(timeoutMs);
211
+ return undefined;
212
+ }
213
+
214
+ function withHardTimeout(operation, timeoutMs, createError) {
215
+ return new Promise((resolve, reject) => {
216
+ const timer = setTimeout(() => reject(createError()), timeoutMs);
217
+ timer.unref?.();
218
+ Promise.resolve()
219
+ .then(operation)
220
+ .then(
221
+ (value) => {
222
+ clearTimeout(timer);
223
+ resolve(value);
224
+ },
225
+ (error) => {
226
+ clearTimeout(timer);
227
+ reject(error);
228
+ },
229
+ );
230
+ });
231
+ }
232
+
233
+ export async function probeEndpoint(rawUrl, options = {}) {
234
+ const url = safeDiagnosticUrl(rawUrl);
235
+ const parsed = new URL(url);
236
+ const lookup = options.lookup || ((hostname) => dns.promises.lookup(hostname, { all: true }));
237
+ const fetchImpl = options.fetchImpl || fetch;
238
+ const runtime = networkEnvironmentSummary(options.environment);
239
+
240
+ return withNetworkRetry(async ({ attempt, maxAttempts }) => {
241
+ const startedAt = Date.now();
242
+ let dnsRecords = [];
243
+ try {
244
+ const timeoutMs = Math.max(1, Number(options.timeoutMs) || 10_000);
245
+ dnsRecords = normalizedDnsResults(await withHardTimeout(
246
+ () => lookup(parsed.hostname),
247
+ timeoutMs,
248
+ () => Object.assign(new Error(`DNS lookup timed out for ${parsed.hostname}`), {
249
+ code: 'ETIMEDOUT',
250
+ syscall: 'dns.lookup',
251
+ hostname: parsed.hostname,
252
+ }),
253
+ ));
254
+ const response = await fetchImpl(url, {
255
+ method: 'GET',
256
+ redirect: 'follow',
257
+ signal: fetchTimeoutSignal(timeoutMs),
258
+ });
259
+ try {
260
+ const event = {
261
+ type: 'network.probe',
262
+ timestamp: new Date().toISOString(),
263
+ url,
264
+ hostname: parsed.hostname,
265
+ attempt,
266
+ maxAttempts,
267
+ durationMs: Date.now() - startedAt,
268
+ dns: dnsRecords,
269
+ status: Number(response.status),
270
+ statusText: String(response.statusText || ''),
271
+ category: response.ok ? 'ok' : classifyNetworkError(Object.assign(new Error(`HTTP ${response.status}`), { status: response.status })),
272
+ ...runtime,
273
+ };
274
+ await options.onAttempt?.(event);
275
+ if (!response.ok) {
276
+ throw Object.assign(new Error(`HTTP ${response.status} ${response.statusText || ''}`.trim()), {
277
+ status: response.status,
278
+ });
279
+ }
280
+ return event;
281
+ } finally {
282
+ await response.body?.cancel?.().catch(() => {});
283
+ }
284
+ } catch (error) {
285
+ if (errorStatus(error) === undefined) {
286
+ await options.onAttempt?.({
287
+ type: 'network.probe',
288
+ timestamp: new Date().toISOString(),
289
+ url,
290
+ hostname: parsed.hostname,
291
+ attempt,
292
+ maxAttempts,
293
+ durationMs: Date.now() - startedAt,
294
+ dns: dnsRecords,
295
+ category: classifyNetworkError(error),
296
+ error: describeNetworkError(error),
297
+ ...runtime,
298
+ });
299
+ }
300
+ throw error;
301
+ }
302
+ }, {
303
+ maxAttempts: options.maxAttempts ?? 3,
304
+ baseDelayMs: options.baseDelayMs ?? 500,
305
+ sleep: options.sleep,
306
+ onRetry: options.onRetry,
307
+ });
308
+ }
309
+
310
+ export function createSerializedLineWriter(append) {
311
+ let pending = Promise.resolve();
312
+ let firstError;
313
+ return {
314
+ write(line) {
315
+ const current = pending.then(() => append(line));
316
+ pending = current.catch((error) => {
317
+ firstError ??= error;
318
+ });
319
+ return current;
320
+ },
321
+ async flush() {
322
+ await pending;
323
+ if (firstError) throw firstError;
324
+ },
325
+ };
326
+ }