luckytri 1.0.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.
- package/.github/SECURITY.md +71 -0
- package/LICENSE +21 -0
- package/README.en.md +250 -0
- package/README.md +250 -0
- package/bin/luckytri.js +107 -0
- package/docs/brand/banner.png +0 -0
- package/docs/brand/palette.png +0 -0
- package/docs/brand/portrait.png +0 -0
- package/docs/en/CHANGELOG.md +113 -0
- package/docs/en/architecture.md +152 -0
- package/docs/en/chat-style.md +62 -0
- package/docs/en/connect.md +63 -0
- package/docs/en/guide.md +216 -0
- package/docs/en/her-life.md +189 -0
- package/docs/en/npm-release.md +25 -0
- package/docs/en/plugins.md +82 -0
- package/docs/en/recovery.md +28 -0
- package/docs/en/time.md +71 -0
- package/docs/en/webui.md +73 -0
- package/docs/zh/CHANGELOG.md +113 -0
- package/docs/zh/architecture.md +152 -0
- package/docs/zh/chat-style.md +62 -0
- package/docs/zh/connect.md +63 -0
- package/docs/zh/guide.md +216 -0
- package/docs/zh/her-life.md +189 -0
- package/docs/zh/npm-release.md +33 -0
- package/docs/zh/plugins.md +82 -0
- package/docs/zh/recovery.md +28 -0
- package/docs/zh/time.md +71 -0
- package/docs/zh/webui.md +73 -0
- package/package.json +108 -0
- package/plugins/sdk/index.d.ts +155 -0
- package/plugins/sdk/testing.js +58 -0
- package/plugins/weather/index.js +82 -0
- package/plugins/weather/luckytri-plugin.json +29 -0
- package/public/app/assets/ChatsPage-C-FcRapR.js +3 -0
- package/public/app/assets/ChatsPage-DfyPCS9N.css +1 -0
- package/public/app/assets/Empty-CpP5kQHH.js +2 -0
- package/public/app/assets/Empty-DmUB2-Bk.css +1 -0
- package/public/app/assets/Field-D4gY-INU.js +1 -0
- package/public/app/assets/HeartPage-Bft9wZp5.css +1 -0
- package/public/app/assets/HeartPage-CPy_thQd.js +1 -0
- package/public/app/assets/LifePage-BUi5euBF.css +1 -0
- package/public/app/assets/LifePage-CxWvwmSf.js +1 -0
- package/public/app/assets/MemoryPage-DC4rhQ68.js +1 -0
- package/public/app/assets/MemoryPage-cZvXfkXb.css +1 -0
- package/public/app/assets/NaturePage-CTXHCE2D.css +1 -0
- package/public/app/assets/NaturePage-IvYw04Zq.js +3 -0
- package/public/app/assets/PeoplePage-BmUxkrOU.css +1 -0
- package/public/app/assets/PeoplePage-Cqefm9-q.js +1 -0
- package/public/app/assets/PluginsPage-DKSaKvXb.js +2 -0
- package/public/app/assets/PluginsPage-HyBe5D8w.css +1 -0
- package/public/app/assets/SystemPage-CAMPoYqM.css +1 -0
- package/public/app/assets/SystemPage-CJmtJkrK.js +1 -0
- package/public/app/assets/TimePage-BOms_n4I.css +1 -0
- package/public/app/assets/TimePage-Cw44inM4.js +3 -0
- package/public/app/assets/formatters-ZmW_xFHw.js +1 -0
- package/public/app/assets/index-BheRW-1t.js +4 -0
- package/public/app/assets/index-YDz1ree1.css +1 -0
- package/public/app/assets/knowledge-DG0e0uKw.js +1 -0
- package/public/app/assets/liquid-garden-IMoHBwzc.png +0 -0
- package/public/app/assets/luckytri-home-95QKeMtc.png +0 -0
- package/public/app/assets/media-BzPDBm7e.js +1 -0
- package/public/app/index.html +27 -0
- package/public/guide.en.html +1053 -0
- package/public/guide.html +893 -0
- package/public/plugin-kit/v1/bridge.js +30 -0
- package/public/plugin-kit/v1/kit.css +14 -0
- package/public/tutorial.en.md +214 -0
- package/public/tutorial.md +214 -0
- package/scripts/backup.js +311 -0
- package/scripts/browser.js +41 -0
- package/scripts/launch.js +94 -0
- package/scripts/plugin.js +150 -0
- package/scripts/proxy-config.js +167 -0
- package/scripts/recover.js +26 -0
- package/scripts/runtime.js +25 -0
- package/scripts/service.js +47 -0
- package/scripts/setup.js +50 -0
- package/scripts/start-server.js +41 -0
- package/scripts/stop.js +36 -0
- package/server/app.js +96 -0
- package/server/channels/adapters.js +27 -0
- package/server/channels/contract.js +57 -0
- package/server/channels/hub.js +165 -0
- package/server/channels/index.js +25 -0
- package/server/channels/onebot/adapter.js +288 -0
- package/server/channels/onebot/index.js +336 -0
- package/server/channels/onebot/token.js +3 -0
- package/server/channels/qqbot/adapter.js +253 -0
- package/server/channels/qqbot/api.js +199 -0
- package/server/channels/qqbot/credentials.js +11 -0
- package/server/channels/qqbot/gateway.js +274 -0
- package/server/channels/qqbot/index.js +389 -0
- package/server/channels/segments.js +40 -0
- package/server/channels/session-key.js +153 -0
- package/server/channels/state.js +42 -0
- package/server/core/api.js +564 -0
- package/server/core/capabilities.js +208 -0
- package/server/core/context-builder.js +288 -0
- package/server/core/context-compactor.js +454 -0
- package/server/core/conversation-cues.js +325 -0
- package/server/core/conversation-manager.js +87 -0
- package/server/core/events.js +60 -0
- package/server/core/feedback.js +8 -0
- package/server/core/group-style.js +72 -0
- package/server/core/image-preprocessor.js +208 -0
- package/server/core/initiative-context.js +66 -0
- package/server/core/input-budget.js +32 -0
- package/server/core/local-demo.js +98 -0
- package/server/core/message-manager.js +118 -0
- package/server/core/message-scheduler.js +78 -0
- package/server/core/model-json.js +190 -0
- package/server/core/model-manager.js +1213 -0
- package/server/core/model-presets.js +900 -0
- package/server/core/name-call.js +29 -0
- package/server/core/net-guard.js +49 -0
- package/server/core/network.js +157 -0
- package/server/core/orchestrator.js +569 -0
- package/server/core/perceivers.js +91 -0
- package/server/core/person-name.js +22 -0
- package/server/core/persona-manager.js +243 -0
- package/server/core/reply-delivery.js +393 -0
- package/server/core/reply-target-resolver.js +50 -0
- package/server/core/repository.js +293 -0
- package/server/core/response-generator.js +50 -0
- package/server/core/response-validator.js +508 -0
- package/server/core/retired-prompts.js +51 -0
- package/server/core/sessions.js +109 -0
- package/server/core/speaker-names.js +92 -0
- package/server/core/topic-tracker.js +65 -0
- package/server/core/turn-processor.js +630 -0
- package/server/core/turn.js +236 -0
- package/server/core/vision-manager.js +633 -0
- package/server/http.js +18 -0
- package/server/i18n/decisions.js +206 -0
- package/server/i18n/errors.js +497 -0
- package/server/i18n/index.js +144 -0
- package/server/i18n/labels.js +653 -0
- package/server/i18n/models.js +111 -0
- package/server/index.js +102 -0
- package/server/knowledge/api.js +342 -0
- package/server/knowledge/embedding-profile.js +114 -0
- package/server/knowledge/index.js +13 -0
- package/server/knowledge/manager.js +386 -0
- package/server/knowledge/retrieval.js +93 -0
- package/server/knowledge/schema.js +154 -0
- package/server/knowledge/vector-index.js +90 -0
- package/server/mind/affect.js +265 -0
- package/server/mind/anticipations.js +507 -0
- package/server/mind/api.js +703 -0
- package/server/mind/attention.js +200 -0
- package/server/mind/bonds.js +558 -0
- package/server/mind/budget.js +152 -0
- package/server/mind/clock.js +83 -0
- package/server/mind/context-selection.js +61 -0
- package/server/mind/continuity.js +562 -0
- package/server/mind/conversation-origin.js +175 -0
- package/server/mind/day-planner.js +324 -0
- package/server/mind/days.js +210 -0
- package/server/mind/evidence-read.js +45 -0
- package/server/mind/evidence.js +148 -0
- package/server/mind/faces.js +138 -0
- package/server/mind/guard.js +60 -0
- package/server/mind/index.js +445 -0
- package/server/mind/initiative.js +333 -0
- package/server/mind/life/activities.js +21 -0
- package/server/mind/life/context.js +309 -0
- package/server/mind/life/diary.js +786 -0
- package/server/mind/life/growth.js +252 -0
- package/server/mind/life/index.js +1049 -0
- package/server/mind/life/presence.js +305 -0
- package/server/mind/meetings.js +1069 -0
- package/server/mind/memory-policy.js +48 -0
- package/server/mind/memory.js +888 -0
- package/server/mind/nature.js +262 -0
- package/server/mind/novelty.js +19 -0
- package/server/mind/observations.js +84 -0
- package/server/mind/own-day.js +324 -0
- package/server/mind/own-voice.js +318 -0
- package/server/mind/periods.js +118 -0
- package/server/mind/reading.js +120 -0
- package/server/mind/salience.js +201 -0
- package/server/mind/schema.js +562 -0
- package/server/mind/self.js +484 -0
- package/server/mind/senses.js +53 -0
- package/server/mind/thoughts.js +274 -0
- package/server/mind/time/activity-clock.js +196 -0
- package/server/mind/time/agenda.js +242 -0
- package/server/mind/time/api.js +200 -0
- package/server/mind/time/attention.js +34 -0
- package/server/mind/time/availability.js +69 -0
- package/server/mind/time/commitment-review.js +198 -0
- package/server/mind/time/delivery-links.js +130 -0
- package/server/mind/time/executor.js +512 -0
- package/server/mind/time/experiences.js +128 -0
- package/server/mind/time/games.js +562 -0
- package/server/mind/time/index.js +739 -0
- package/server/mind/time/intent.js +166 -0
- package/server/mind/time/kinds.js +171 -0
- package/server/mind/time/model-material.js +77 -0
- package/server/mind/time/plugin-activity.js +207 -0
- package/server/mind/time/presentation.js +51 -0
- package/server/mind/time/schedule.js +83 -0
- package/server/mind/time/schema.js +84 -0
- package/server/mind/time/search.js +383 -0
- package/server/mind/time/sharing.js +472 -0
- package/server/mind/time/task-links.js +311 -0
- package/server/mind/time/tasks.js +587 -0
- package/server/mind/time/works.js +432 -0
- package/server/mind/traits.js +191 -0
- package/server/mind/util.js +110 -0
- package/server/mind/view.js +273 -0
- package/server/paths.js +102 -0
- package/server/plugins/api.js +204 -0
- package/server/plugins/host.js +967 -0
- package/server/plugins/install.js +295 -0
- package/server/plugins/manifest.js +201 -0
- package/server/plugins/permissions.js +42 -0
- package/server/plugins/runner/guard.mjs +36 -0
- package/server/plugins/runner/worker.mjs +250 -0
- package/server/plugins/schema.js +91 -0
- package/server/plugins/zip.js +112 -0
- package/server/startup-lock.js +118 -0
- package/server/storage/archive.js +241 -0
- package/server/storage/backup-cleanup-worker.js +8 -0
- package/server/storage/backup-cleanup.js +414 -0
- package/server/storage/backup-lock.js +76 -0
- package/server/storage/backup-scheduler.js +131 -0
- package/server/storage/store.js +199 -0
- package/server/studio/display-names.js +138 -0
- package/server/studio/management.js +102 -0
- package/server/studio/readiness.js +190 -0
- package/server/studio/settings.js +236 -0
- package/server/version.js +10 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const nameCallBoundary = "\\s,,、。!?!?::;;()()\\[\\]";
|
|
2
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3
|
+
const HAIL = "喂哎诶嘿";
|
|
4
|
+
const PARTICLES = "啊呀呢吧哦嘛";
|
|
5
|
+
const AFTER = "你|我|今天|现在|怎么|能|可以|帮|回|看|咋|在|有没有|好累|辛苦|救";
|
|
6
|
+
// Talking about her, not to her. Checked against the text after the name.
|
|
7
|
+
const ABOUT =
|
|
8
|
+
/^(?:\s*(?:是谁|是个|是一个|的名字|这个名字|这人|那个人|的?头像|在群里|说的是))/i;
|
|
9
|
+
|
|
10
|
+
function calledBy(value, name) {
|
|
11
|
+
if (!name) return false;
|
|
12
|
+
const call = new RegExp(
|
|
13
|
+
`(?:^|[${nameCallBoundary}]|[${HAIL}])@?${escapeRegExp(name)}(?=$|[${nameCallBoundary}]|[${PARTICLES}]|(?:${AFTER}))`,
|
|
14
|
+
"gi",
|
|
15
|
+
);
|
|
16
|
+
let match;
|
|
17
|
+
while ((match = call.exec(value))) {
|
|
18
|
+
const after = value.slice(match.index + match[0].length);
|
|
19
|
+
if (!ABOUT.test(after)) return true;
|
|
20
|
+
if (match.index === call.lastIndex) call.lastIndex += 1;
|
|
21
|
+
}
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function isNameCall(text, names = []) {
|
|
26
|
+
const value = String(text || "").trim();
|
|
27
|
+
if (!value) return false;
|
|
28
|
+
return names.some((raw) => calledBy(value, String(raw || "").trim()));
|
|
29
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
|
|
3
|
+
// Addresses a plugin or a downloaded image must never reach: the machine
|
|
4
|
+
// itself, the local network, and the cloud metadata range. Shared so a
|
|
5
|
+
// plugin's fetch and a picture download refuse the same places.
|
|
6
|
+
export function blockedAddress(address) {
|
|
7
|
+
const raw = String(address || "")
|
|
8
|
+
.toLowerCase()
|
|
9
|
+
.replace(/^\[|\]$/g, "");
|
|
10
|
+
const mapped = raw.startsWith("::ffff:") ? raw.slice(7) : raw;
|
|
11
|
+
if (mapped === "::1" || mapped === "::" || raw === "::1" || raw === "::")
|
|
12
|
+
return true;
|
|
13
|
+
if (raw.startsWith("fe80:") || raw.startsWith("fc") || raw.startsWith("fd"))
|
|
14
|
+
return true;
|
|
15
|
+
const ip = isIP(mapped) === 4 ? mapped : isIP(raw) === 4 ? raw : "";
|
|
16
|
+
if (!ip) return isIP(raw) !== 6;
|
|
17
|
+
const [a, b] = ip.split(".").map(Number);
|
|
18
|
+
return (
|
|
19
|
+
a === 0 ||
|
|
20
|
+
a === 10 ||
|
|
21
|
+
a === 127 ||
|
|
22
|
+
(a === 169 && b === 254) ||
|
|
23
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
24
|
+
(a === 192 && b === 168) ||
|
|
25
|
+
(a === 100 && b >= 64 && b <= 127)
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function assertPublicHost(
|
|
30
|
+
hostname,
|
|
31
|
+
lookupHost,
|
|
32
|
+
deny = "地址不可访问",
|
|
33
|
+
) {
|
|
34
|
+
const host = String(hostname || "").replace(/^\[|\]$/g, "");
|
|
35
|
+
if (!host) throw Error(deny);
|
|
36
|
+
if (isIP(host)) {
|
|
37
|
+
if (blockedAddress(host)) throw Error(deny);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let records;
|
|
41
|
+
try {
|
|
42
|
+
records = await lookupHost(host, { all: true });
|
|
43
|
+
} catch {
|
|
44
|
+
throw Error(deny);
|
|
45
|
+
}
|
|
46
|
+
const list = Array.isArray(records) ? records : [records];
|
|
47
|
+
if (!list.length || list.some((item) => blockedAddress(item?.address)))
|
|
48
|
+
throw Error(deny);
|
|
49
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const TRANSIENT_NETWORK_CODES = new Set([
|
|
2
|
+
"ECONNRESET",
|
|
3
|
+
"ECONNREFUSED",
|
|
4
|
+
"ECONNABORTED",
|
|
5
|
+
"EPIPE",
|
|
6
|
+
"EHOSTUNREACH",
|
|
7
|
+
"ENETUNREACH",
|
|
8
|
+
"EAI_AGAIN",
|
|
9
|
+
"ETIMEDOUT",
|
|
10
|
+
"UND_ERR_CONNECT_TIMEOUT",
|
|
11
|
+
"UND_ERR_HEADERS_TIMEOUT",
|
|
12
|
+
"UND_ERR_BODY_TIMEOUT",
|
|
13
|
+
"UND_ERR_SOCKET",
|
|
14
|
+
"UND_ERR_CLOSED",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export const RETRYABLE_STATUS = new Set([
|
|
18
|
+
408, 425, 429, 500, 502, 503, 504, 520, 521, 522, 523, 529,
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
const TLS_CODE =
|
|
22
|
+
/^(?:ERR_SSL|ERR_TLS|EPROTO|CERT_|UNABLE_TO_VERIFY|DEPTH_ZERO|SELF_SIGNED)/;
|
|
23
|
+
|
|
24
|
+
export function isTransientNetworkError(error) {
|
|
25
|
+
let current = error;
|
|
26
|
+
for (let depth = 0; current && depth < 6; depth++, current = current.cause) {
|
|
27
|
+
if (
|
|
28
|
+
TRANSIENT_NETWORK_CODES.has(current.code) ||
|
|
29
|
+
TRANSIENT_NETWORK_CODES.has(current.errno)
|
|
30
|
+
)
|
|
31
|
+
return true;
|
|
32
|
+
if (
|
|
33
|
+
current.name === "TypeError" &&
|
|
34
|
+
/^(?:fetch failed|network error)$/i.test(String(current.message || ""))
|
|
35
|
+
)
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function isTimeoutError(error) {
|
|
42
|
+
let current = error;
|
|
43
|
+
for (let depth = 0; current && depth < 6; depth++, current = current.cause)
|
|
44
|
+
if (current.name === "TimeoutError") return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function sanitizeDetail(text, max = 300) {
|
|
49
|
+
return String(text || "")
|
|
50
|
+
.replace(/Bearer\s+[^\s,;]+/gi, "Bearer [已隐藏]")
|
|
51
|
+
.replace(/((?:api[_ -]?key|token|secret)\s*[:= ]+)[^\s,;]+/gi, "$1[已隐藏]")
|
|
52
|
+
.replace(/\bsk-[A-Za-z0-9_-]{8,}/g, "sk-[已隐藏]")
|
|
53
|
+
.replace(/https?:\/\/[^\s"'<>]+/gi, (url) => {
|
|
54
|
+
try {
|
|
55
|
+
return new URL(url).origin;
|
|
56
|
+
} catch {
|
|
57
|
+
return "[地址]";
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.replace(/[\r\n\t]+/g, " ")
|
|
61
|
+
.trim()
|
|
62
|
+
.slice(0, max);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Walks the fetch cause chain so a proxy tunnel refusal, a TLS failure and a
|
|
66
|
+
// server reset are reported as different problems instead of "fetch failed".
|
|
67
|
+
export function describeNetworkError(error) {
|
|
68
|
+
const chain = [];
|
|
69
|
+
for (
|
|
70
|
+
let current = error, depth = 0;
|
|
71
|
+
current && depth < 6;
|
|
72
|
+
depth++, current = current.cause
|
|
73
|
+
)
|
|
74
|
+
chain.push(current);
|
|
75
|
+
if (isTimeoutError(error)) return { code: "TIMEOUT", detail: "" };
|
|
76
|
+
const messages = chain
|
|
77
|
+
.map((item) => String(item?.message || ""))
|
|
78
|
+
.filter(Boolean);
|
|
79
|
+
let code =
|
|
80
|
+
chain
|
|
81
|
+
.map((item) => item?.code || item?.errno)
|
|
82
|
+
.find((value) => TRANSIENT_NETWORK_CODES.has(value)) || "";
|
|
83
|
+
const proxy = messages
|
|
84
|
+
.map((message) => message.match(/Proxy response \((\d{3})\)/i))
|
|
85
|
+
.find(Boolean);
|
|
86
|
+
if (!code && proxy) code = `PROXY_${proxy[1]}`;
|
|
87
|
+
if (!code)
|
|
88
|
+
code =
|
|
89
|
+
chain
|
|
90
|
+
.map((item) => String(item?.code || ""))
|
|
91
|
+
.find((value) => TLS_CODE.test(value)) || "";
|
|
92
|
+
if (!code)
|
|
93
|
+
code =
|
|
94
|
+
chain
|
|
95
|
+
.map((item) => item?.code)
|
|
96
|
+
.find((value) => typeof value === "string" && value) || "";
|
|
97
|
+
if (!code && isTransientNetworkError(error)) code = "FETCH_FAILED";
|
|
98
|
+
const deepest =
|
|
99
|
+
messages.filter((message) => !/^fetch failed$/i.test(message)).at(-1) || "";
|
|
100
|
+
return { code, detail: sanitizeDetail(deepest, 160) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function networkErrorCode(error) {
|
|
104
|
+
return describeNetworkError(error).code;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function retryDelayFromResponse(response, fallbackMs = 900) {
|
|
108
|
+
const value = String(response?.headers?.get?.("retry-after") || "").trim();
|
|
109
|
+
if (value) {
|
|
110
|
+
const seconds = Number(value);
|
|
111
|
+
const dateDelay = Date.parse(value) - Date.now();
|
|
112
|
+
const delay = Number.isFinite(seconds)
|
|
113
|
+
? seconds * 1000
|
|
114
|
+
: Number.isFinite(dateDelay) && dateDelay > 0
|
|
115
|
+
? dateDelay
|
|
116
|
+
: Number.NaN;
|
|
117
|
+
if (Number.isFinite(delay)) return Math.max(0, Math.min(delay, 30000));
|
|
118
|
+
}
|
|
119
|
+
return Math.max(0, Math.min(Number(fallbackMs) || 900, 30000));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function withTransientRequestRetry(
|
|
123
|
+
operation,
|
|
124
|
+
{
|
|
125
|
+
retries = 3,
|
|
126
|
+
delayMs = 600,
|
|
127
|
+
maxDelayMs = 8000,
|
|
128
|
+
deadlineMs = 45000,
|
|
129
|
+
onRetry,
|
|
130
|
+
random = Math.random,
|
|
131
|
+
sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
132
|
+
now = Date.now,
|
|
133
|
+
} = {},
|
|
134
|
+
) {
|
|
135
|
+
const started = now();
|
|
136
|
+
for (let attempt = 0; ; attempt++) {
|
|
137
|
+
try {
|
|
138
|
+
return await operation(attempt);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
if (
|
|
141
|
+
(!isTransientNetworkError(error) && error?.retryableRequest !== true) ||
|
|
142
|
+
attempt >= retries
|
|
143
|
+
)
|
|
144
|
+
throw error;
|
|
145
|
+
// Half fixed, half random: bursts of failures spread out instead of
|
|
146
|
+
// hitting a recovering gateway at the same moment.
|
|
147
|
+
const ceiling = Math.min(maxDelayMs, delayMs * 2 ** attempt);
|
|
148
|
+
let wait = ceiling / 2 + random() * (ceiling / 2);
|
|
149
|
+
const hinted = Number(error?.retryDelayMs);
|
|
150
|
+
if (Number.isFinite(hinted))
|
|
151
|
+
wait = Math.max(wait, Math.min(hinted, 30000));
|
|
152
|
+
if (now() - started + wait > deadlineMs) throw error;
|
|
153
|
+
onRetry?.(attempt + 1, error, wait);
|
|
154
|
+
await sleep(wait);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|