@hybridaione/hybridclaw 0.2.2 → 0.2.6
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/workflows/ci.yml +70 -0
- package/.husky/pre-commit +1 -0
- package/CHANGELOG.md +85 -0
- package/CONTRIBUTING.md +33 -0
- package/README.md +41 -16
- package/SECURITY.md +17 -0
- package/biome.json +35 -0
- package/config.example.json +71 -8
- package/container/package-lock.json +2 -2
- package/container/package.json +1 -1
- package/container/src/approval-policy.ts +1303 -0
- package/container/src/browser-tools.ts +431 -136
- package/container/src/extensions.ts +36 -12
- package/container/src/hybridai-client.ts +34 -13
- package/container/src/index.ts +451 -109
- package/container/src/ipc.ts +5 -3
- package/container/src/token-usage.ts +20 -10
- package/container/src/tools.ts +599 -225
- package/container/src/types.ts +32 -2
- package/container/src/web-fetch.ts +89 -32
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +10 -2
- package/dist/agent.js.map +1 -1
- package/dist/audit-cli.d.ts.map +1 -1
- package/dist/audit-cli.js +4 -2
- package/dist/audit-cli.js.map +1 -1
- package/dist/audit-events.d.ts.map +1 -1
- package/dist/audit-events.js +53 -3
- package/dist/audit-events.js.map +1 -1
- package/dist/audit-trail.d.ts.map +1 -1
- package/dist/audit-trail.js +17 -8
- package/dist/audit-trail.js.map +1 -1
- package/dist/channels/discord/attachments.d.ts.map +1 -1
- package/dist/channels/discord/attachments.js +14 -7
- package/dist/channels/discord/attachments.js.map +1 -1
- package/dist/channels/discord/debounce.d.ts +9 -0
- package/dist/channels/discord/debounce.d.ts.map +1 -0
- package/dist/channels/discord/debounce.js +20 -0
- package/dist/channels/discord/debounce.js.map +1 -0
- package/dist/channels/discord/delivery.d.ts +4 -1
- package/dist/channels/discord/delivery.d.ts.map +1 -1
- package/dist/channels/discord/delivery.js +19 -3
- package/dist/channels/discord/delivery.js.map +1 -1
- package/dist/channels/discord/human-delay.d.ts +16 -0
- package/dist/channels/discord/human-delay.d.ts.map +1 -0
- package/dist/channels/discord/human-delay.js +29 -0
- package/dist/channels/discord/human-delay.js.map +1 -0
- package/dist/channels/discord/inbound.d.ts +4 -0
- package/dist/channels/discord/inbound.d.ts.map +1 -1
- package/dist/channels/discord/inbound.js +45 -4
- package/dist/channels/discord/inbound.js.map +1 -1
- package/dist/channels/discord/mentions.d.ts.map +1 -1
- package/dist/channels/discord/mentions.js +16 -4
- package/dist/channels/discord/mentions.js.map +1 -1
- package/dist/channels/discord/presence.d.ts +33 -0
- package/dist/channels/discord/presence.d.ts.map +1 -0
- package/dist/channels/discord/presence.js +111 -0
- package/dist/channels/discord/presence.js.map +1 -0
- package/dist/channels/discord/rate-limiter.d.ts +14 -0
- package/dist/channels/discord/rate-limiter.d.ts.map +1 -0
- package/dist/channels/discord/rate-limiter.js +49 -0
- package/dist/channels/discord/rate-limiter.js.map +1 -0
- package/dist/channels/discord/reactions.d.ts +38 -0
- package/dist/channels/discord/reactions.d.ts.map +1 -0
- package/dist/channels/discord/reactions.js +151 -0
- package/dist/channels/discord/reactions.js.map +1 -0
- package/dist/channels/discord/runtime.d.ts +6 -3
- package/dist/channels/discord/runtime.d.ts.map +1 -1
- package/dist/channels/discord/runtime.js +621 -125
- package/dist/channels/discord/runtime.js.map +1 -1
- package/dist/channels/discord/stream.d.ts +4 -1
- package/dist/channels/discord/stream.d.ts.map +1 -1
- package/dist/channels/discord/stream.js +16 -8
- package/dist/channels/discord/stream.js.map +1 -1
- package/dist/channels/discord/tool-actions.d.ts.map +1 -1
- package/dist/channels/discord/tool-actions.js +24 -12
- package/dist/channels/discord/tool-actions.js.map +1 -1
- package/dist/channels/discord/typing.d.ts +15 -0
- package/dist/channels/discord/typing.d.ts.map +1 -0
- package/dist/channels/discord/typing.js +106 -0
- package/dist/channels/discord/typing.js.map +1 -0
- package/dist/chunk.d.ts.map +1 -1
- package/dist/chunk.js +4 -2
- package/dist/chunk.js.map +1 -1
- package/dist/cli.js +47 -22
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +103 -18
- package/dist/config.js.map +1 -1
- package/dist/container-runner.d.ts.map +1 -1
- package/dist/container-runner.js +58 -26
- package/dist/container-runner.js.map +1 -1
- package/dist/container-setup.d.ts.map +1 -1
- package/dist/container-setup.js +10 -9
- package/dist/container-setup.js.map +1 -1
- package/dist/conversation.d.ts +2 -2
- package/dist/conversation.d.ts.map +1 -1
- package/dist/conversation.js +1 -1
- package/dist/conversation.js.map +1 -1
- package/dist/db.d.ts +118 -2
- package/dist/db.d.ts.map +1 -1
- package/dist/db.js +1568 -50
- package/dist/db.js.map +1 -1
- package/dist/delegation-manager.d.ts.map +1 -1
- package/dist/delegation-manager.js +3 -2
- package/dist/delegation-manager.js.map +1 -1
- package/dist/gateway-client.d.ts +2 -2
- package/dist/gateway-client.d.ts.map +1 -1
- package/dist/gateway-client.js +10 -4
- package/dist/gateway-client.js.map +1 -1
- package/dist/gateway-service.d.ts +3 -3
- package/dist/gateway-service.d.ts.map +1 -1
- package/dist/gateway-service.js +563 -73
- package/dist/gateway-service.js.map +1 -1
- package/dist/gateway-types.d.ts +24 -0
- package/dist/gateway-types.d.ts.map +1 -1
- package/dist/gateway-types.js.map +1 -1
- package/dist/gateway.js +179 -24
- package/dist/gateway.js.map +1 -1
- package/dist/health.d.ts.map +1 -1
- package/dist/health.js +20 -10
- package/dist/health.js.map +1 -1
- package/dist/heartbeat.d.ts +4 -0
- package/dist/heartbeat.d.ts.map +1 -1
- package/dist/heartbeat.js +48 -20
- package/dist/heartbeat.js.map +1 -1
- package/dist/hybridai-bots.d.ts.map +1 -1
- package/dist/hybridai-bots.js +4 -2
- package/dist/hybridai-bots.js.map +1 -1
- package/dist/instruction-approval-audit.d.ts.map +1 -1
- package/dist/instruction-approval-audit.js.map +1 -1
- package/dist/instruction-integrity.d.ts.map +1 -1
- package/dist/instruction-integrity.js +8 -2
- package/dist/instruction-integrity.js.map +1 -1
- package/dist/ipc.d.ts.map +1 -1
- package/dist/ipc.js +6 -1
- package/dist/ipc.js.map +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/memory-consolidation.d.ts +17 -0
- package/dist/memory-consolidation.d.ts.map +1 -0
- package/dist/memory-consolidation.js +25 -0
- package/dist/memory-consolidation.js.map +1 -0
- package/dist/memory-service.d.ts +200 -0
- package/dist/memory-service.d.ts.map +1 -0
- package/dist/memory-service.js +294 -0
- package/dist/memory-service.js.map +1 -0
- package/dist/mount-security.d.ts.map +1 -1
- package/dist/mount-security.js +31 -7
- package/dist/mount-security.js.map +1 -1
- package/dist/observability-ingest.d.ts.map +1 -1
- package/dist/observability-ingest.js +32 -11
- package/dist/observability-ingest.js.map +1 -1
- package/dist/onboarding.d.ts.map +1 -1
- package/dist/onboarding.js +32 -9
- package/dist/onboarding.js.map +1 -1
- package/dist/proactive-policy.d.ts.map +1 -1
- package/dist/proactive-policy.js +2 -1
- package/dist/proactive-policy.js.map +1 -1
- package/dist/prompt-hooks.d.ts.map +1 -1
- package/dist/prompt-hooks.js +9 -7
- package/dist/prompt-hooks.js.map +1 -1
- package/dist/runtime-config.d.ts +98 -1
- package/dist/runtime-config.d.ts.map +1 -1
- package/dist/runtime-config.js +477 -23
- package/dist/runtime-config.js.map +1 -1
- package/dist/scheduled-task-runner.d.ts +1 -0
- package/dist/scheduled-task-runner.d.ts.map +1 -1
- package/dist/scheduled-task-runner.js +29 -10
- package/dist/scheduled-task-runner.js.map +1 -1
- package/dist/scheduler.d.ts +43 -4
- package/dist/scheduler.d.ts.map +1 -1
- package/dist/scheduler.js +530 -56
- package/dist/scheduler.js.map +1 -1
- package/dist/session-export.d.ts +26 -0
- package/dist/session-export.d.ts.map +1 -0
- package/dist/session-export.js +149 -0
- package/dist/session-export.js.map +1 -0
- package/dist/session-maintenance.d.ts.map +1 -1
- package/dist/session-maintenance.js +75 -13
- package/dist/session-maintenance.js.map +1 -1
- package/dist/session-transcripts.d.ts.map +1 -1
- package/dist/session-transcripts.js.map +1 -1
- package/dist/side-effects.d.ts.map +1 -1
- package/dist/side-effects.js +14 -2
- package/dist/side-effects.js.map +1 -1
- package/dist/skills-guard.d.ts.map +1 -1
- package/dist/skills-guard.js +893 -130
- package/dist/skills-guard.js.map +1 -1
- package/dist/skills.d.ts +5 -0
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +29 -15
- package/dist/skills.js.map +1 -1
- package/dist/token-efficiency.d.ts.map +1 -1
- package/dist/token-efficiency.js.map +1 -1
- package/dist/tui.js +92 -11
- package/dist/tui.js.map +1 -1
- package/dist/types.d.ts +146 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +24 -1
- package/dist/types.js.map +1 -1
- package/dist/update.d.ts.map +1 -1
- package/dist/update.js +42 -14
- package/dist/update.js.map +1 -1
- package/dist/workspace.d.ts.map +1 -1
- package/dist/workspace.js +49 -9
- package/dist/workspace.js.map +1 -1
- package/docs/chat.html +9 -3
- package/docs/index.html +37 -13
- package/package.json +8 -2
- package/src/agent.ts +16 -3
- package/src/audit-cli.ts +44 -16
- package/src/audit-events.ts +69 -5
- package/src/audit-trail.ts +41 -15
- package/src/channels/discord/attachments.ts +81 -27
- package/src/channels/discord/debounce.ts +25 -0
- package/src/channels/discord/delivery.ts +57 -13
- package/src/channels/discord/human-delay.ts +48 -0
- package/src/channels/discord/inbound.ts +66 -7
- package/src/channels/discord/mentions.ts +42 -18
- package/src/channels/discord/presence.ts +148 -0
- package/src/channels/discord/rate-limiter.ts +58 -0
- package/src/channels/discord/reactions.ts +211 -0
- package/src/channels/discord/runtime.ts +1048 -182
- package/src/channels/discord/stream.ts +73 -27
- package/src/channels/discord/tool-actions.ts +78 -37
- package/src/channels/discord/typing.ts +140 -0
- package/src/chunk.ts +12 -4
- package/src/cli.ts +141 -56
- package/src/config.ts +192 -34
- package/src/container-runner.ts +132 -42
- package/src/container-setup.ts +57 -22
- package/src/conversation.ts +9 -7
- package/src/db.ts +2217 -84
- package/src/delegation-manager.ts +6 -2
- package/src/gateway-client.ts +41 -17
- package/src/gateway-service.ts +1019 -201
- package/src/gateway-types.ts +33 -0
- package/src/gateway.ts +321 -48
- package/src/health.ts +66 -26
- package/src/heartbeat.ts +84 -22
- package/src/hybridai-bots.ts +14 -5
- package/src/instruction-approval-audit.ts +4 -1
- package/src/instruction-integrity.ts +30 -9
- package/src/ipc.ts +23 -5
- package/src/logger.ts +4 -1
- package/src/memory-consolidation.ts +41 -0
- package/src/memory-service.ts +606 -0
- package/src/mount-security.ts +58 -13
- package/src/observability-ingest.ts +134 -35
- package/src/onboarding.ts +126 -35
- package/src/proactive-policy.ts +3 -1
- package/src/prompt-hooks.ts +40 -17
- package/src/runtime-config.ts +1114 -99
- package/src/scheduled-task-runner.ts +63 -11
- package/src/scheduler.ts +683 -60
- package/src/session-export.ts +196 -0
- package/src/session-maintenance.ts +125 -22
- package/src/session-transcripts.ts +12 -3
- package/src/side-effects.ts +28 -5
- package/src/skills-guard.ts +1067 -219
- package/src/skills.ts +163 -65
- package/src/token-efficiency.ts +31 -9
- package/src/tui.ts +166 -25
- package/src/types.ts +195 -2
- package/src/update.ts +79 -23
- package/src/workspace.ts +63 -11
- package/tests/approval-policy.test.ts +224 -0
- package/tests/discord.basic.test.ts +82 -2
- package/tests/discord.human-presence.test.ts +85 -0
- package/tests/gateway-service.media-routing.test.ts +8 -2
- package/tests/memory-service.test.ts +1114 -0
- package/tests/token-efficiency.basic.test.ts +8 -2
- package/vitest.e2e.config.ts +3 -1
- package/vitest.integration.config.ts +3 -1
- package/vitest.live.config.ts +3 -1
- package/vitest.unit.config.ts +9 -0
package/src/audit-events.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type AuditEventPayload,
|
|
3
|
+
appendAuditEvent,
|
|
4
|
+
createAuditRunId,
|
|
5
|
+
parseJsonObject,
|
|
6
|
+
truncateAuditText,
|
|
7
|
+
} from './audit-trail.js';
|
|
2
8
|
import { logStructuredAuditEvent } from './db.js';
|
|
3
9
|
import { logger } from './logger.js';
|
|
4
10
|
import type { ToolExecution } from './types.js';
|
|
@@ -35,7 +41,8 @@ function summarizeToolResult(text: string): string {
|
|
|
35
41
|
return truncateAuditText(text, 280);
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
const SENSITIVE_ARG_KEY_RE =
|
|
44
|
+
const SENSITIVE_ARG_KEY_RE =
|
|
45
|
+
/(pass(word)?|secret|token|api[_-]?key|authorization|cookie|credential|session)/i;
|
|
39
46
|
|
|
40
47
|
function sanitizeAuditArguments(toolName: string, value: unknown): unknown {
|
|
41
48
|
if (Array.isArray(value)) {
|
|
@@ -69,7 +76,10 @@ export function emitToolExecutionAuditEvents(input: {
|
|
|
69
76
|
toolExecutions.forEach((execution, index) => {
|
|
70
77
|
const toolCallId = `${runId}:tool:${index + 1}`;
|
|
71
78
|
const argumentsObject = parseJsonObject(execution.arguments || '{}');
|
|
72
|
-
const auditArguments = sanitizeAuditArguments(
|
|
79
|
+
const auditArguments = sanitizeAuditArguments(
|
|
80
|
+
execution.name,
|
|
81
|
+
argumentsObject,
|
|
82
|
+
);
|
|
73
83
|
|
|
74
84
|
recordAuditEvent({
|
|
75
85
|
sessionId,
|
|
@@ -90,11 +100,65 @@ export function emitToolExecutionAuditEvents(input: {
|
|
|
90
100
|
action: `tool:${execution.name}`,
|
|
91
101
|
resource: 'container.sandbox',
|
|
92
102
|
allowed: !execution.blocked,
|
|
93
|
-
reason:
|
|
103
|
+
reason:
|
|
104
|
+
execution.blockedReason ||
|
|
105
|
+
execution.approvalReason ||
|
|
106
|
+
(execution.approvalDecision
|
|
107
|
+
? `approval:${execution.approvalDecision}`
|
|
108
|
+
: 'allowed'),
|
|
94
109
|
},
|
|
95
110
|
});
|
|
96
111
|
|
|
97
|
-
|
|
112
|
+
const isRedApprovalAction =
|
|
113
|
+
execution.approvalTier === 'red' || execution.approvalBaseTier === 'red';
|
|
114
|
+
const hasApprovalDecision = typeof execution.approvalDecision === 'string';
|
|
115
|
+
if (isRedApprovalAction || hasApprovalDecision) {
|
|
116
|
+
const description =
|
|
117
|
+
execution.approvalReason ||
|
|
118
|
+
execution.blockedReason ||
|
|
119
|
+
`Approval flow for tool ${execution.name}`;
|
|
120
|
+
recordAuditEvent({
|
|
121
|
+
sessionId,
|
|
122
|
+
runId,
|
|
123
|
+
event: {
|
|
124
|
+
type: 'approval.request',
|
|
125
|
+
toolCallId,
|
|
126
|
+
action: execution.approvalActionKey || `tool:${execution.name}`,
|
|
127
|
+
description,
|
|
128
|
+
policyName: 'trusted-coworker',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const decision = execution.approvalDecision;
|
|
133
|
+
const approved =
|
|
134
|
+
decision === 'approved_once' ||
|
|
135
|
+
decision === 'approved_session' ||
|
|
136
|
+
decision === 'approved_agent' ||
|
|
137
|
+
decision === 'promoted';
|
|
138
|
+
const pending = decision === 'required';
|
|
139
|
+
if (decision && decision !== 'auto' && decision !== 'implicit') {
|
|
140
|
+
recordAuditEvent({
|
|
141
|
+
sessionId,
|
|
142
|
+
runId,
|
|
143
|
+
event: {
|
|
144
|
+
type: 'approval.response',
|
|
145
|
+
toolCallId,
|
|
146
|
+
action: execution.approvalActionKey || `tool:${execution.name}`,
|
|
147
|
+
description: pending
|
|
148
|
+
? `${description} (pending user response)`
|
|
149
|
+
: description,
|
|
150
|
+
approved,
|
|
151
|
+
approvedBy: pending
|
|
152
|
+
? 'pending-user-response'
|
|
153
|
+
: approved
|
|
154
|
+
? 'local-user'
|
|
155
|
+
: 'policy-engine',
|
|
156
|
+
method: pending || approved ? 'prompt' : 'policy',
|
|
157
|
+
policyName: 'trusted-coworker',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
} else if (execution.blocked) {
|
|
98
162
|
recordAuditEvent({
|
|
99
163
|
sessionId,
|
|
100
164
|
runId,
|
package/src/audit-trail.ts
CHANGED
|
@@ -92,10 +92,12 @@ function stableStringify(value: unknown): string {
|
|
|
92
92
|
const type = typeof value;
|
|
93
93
|
|
|
94
94
|
if (type === 'string') return JSON.stringify(value);
|
|
95
|
-
if (type === 'number')
|
|
95
|
+
if (type === 'number')
|
|
96
|
+
return Number.isFinite(value as number) ? String(value) : 'null';
|
|
96
97
|
if (type === 'boolean') return value ? 'true' : 'false';
|
|
97
98
|
if (type === 'bigint') return JSON.stringify((value as bigint).toString());
|
|
98
|
-
if (type === 'undefined' || type === 'function' || type === 'symbol')
|
|
99
|
+
if (type === 'undefined' || type === 'function' || type === 'symbol')
|
|
100
|
+
return 'null';
|
|
99
101
|
|
|
100
102
|
if (Array.isArray(value)) {
|
|
101
103
|
return `[${value.map((entry) => stableStringify(entry === undefined ? null : entry)).join(',')}]`;
|
|
@@ -106,7 +108,12 @@ function stableStringify(value: unknown): string {
|
|
|
106
108
|
const keys = Object.keys(obj).sort((a, b) => a.localeCompare(b));
|
|
107
109
|
for (const key of keys) {
|
|
108
110
|
const raw = obj[key];
|
|
109
|
-
if (
|
|
111
|
+
if (
|
|
112
|
+
raw === undefined ||
|
|
113
|
+
typeof raw === 'function' ||
|
|
114
|
+
typeof raw === 'symbol'
|
|
115
|
+
)
|
|
116
|
+
continue;
|
|
110
117
|
parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
|
|
111
118
|
}
|
|
112
119
|
return `{${parts.join(',')}}`;
|
|
@@ -167,7 +174,10 @@ function computeWireRecordHash(record: Omit<WireRecord, '_hash'>): string {
|
|
|
167
174
|
return sha256(stableStringify(record));
|
|
168
175
|
}
|
|
169
176
|
|
|
170
|
-
function readSessionStateFromDisk(
|
|
177
|
+
function readSessionStateFromDisk(
|
|
178
|
+
sessionId: string,
|
|
179
|
+
filePath: string,
|
|
180
|
+
): SessionAuditState {
|
|
171
181
|
if (!fs.existsSync(filePath) || fs.statSync(filePath).size === 0) {
|
|
172
182
|
const metadata: WireMetadataRecord = {
|
|
173
183
|
type: 'metadata',
|
|
@@ -184,7 +194,10 @@ function readSessionStateFromDisk(sessionId: string, filePath: string): SessionA
|
|
|
184
194
|
}
|
|
185
195
|
|
|
186
196
|
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
187
|
-
const lines = raw
|
|
197
|
+
const lines = raw
|
|
198
|
+
.split('\n')
|
|
199
|
+
.map((line) => line.trim())
|
|
200
|
+
.filter(Boolean);
|
|
188
201
|
if (lines.length === 0) {
|
|
189
202
|
const metadata: WireMetadataRecord = {
|
|
190
203
|
type: 'metadata',
|
|
@@ -209,8 +222,14 @@ function readSessionStateFromDisk(sessionId: string, filePath: string): SessionA
|
|
|
209
222
|
const metadata: WireMetadataRecord = {
|
|
210
223
|
type: 'metadata',
|
|
211
224
|
protocolVersion: AUDIT_PROTOCOL_VERSION,
|
|
212
|
-
sessionId:
|
|
213
|
-
|
|
225
|
+
sessionId:
|
|
226
|
+
typeof firstParsed.sessionId === 'string'
|
|
227
|
+
? firstParsed.sessionId
|
|
228
|
+
: sessionId,
|
|
229
|
+
createdAt:
|
|
230
|
+
typeof firstParsed.createdAt === 'string'
|
|
231
|
+
? firstParsed.createdAt
|
|
232
|
+
: new Date().toISOString(),
|
|
214
233
|
};
|
|
215
234
|
lastHash = computeMetadataHash(metadata);
|
|
216
235
|
startIndex = 1;
|
|
@@ -223,10 +242,10 @@ function readSessionStateFromDisk(sessionId: string, filePath: string): SessionA
|
|
|
223
242
|
try {
|
|
224
243
|
const parsed = JSON.parse(lines[i]) as Partial<WireRecord>;
|
|
225
244
|
if (
|
|
226
|
-
typeof parsed.seq === 'number'
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
245
|
+
typeof parsed.seq === 'number' &&
|
|
246
|
+
Number.isFinite(parsed.seq) &&
|
|
247
|
+
typeof parsed._hash === 'string' &&
|
|
248
|
+
parsed._hash
|
|
230
249
|
) {
|
|
231
250
|
seq = parsed.seq;
|
|
232
251
|
lastHash = parsed._hash;
|
|
@@ -319,7 +338,8 @@ export function verifyAuditSessionChain(sessionId: string): AuditVerifyResult {
|
|
|
319
338
|
const metadata: WireMetadataRecord = {
|
|
320
339
|
type: 'metadata',
|
|
321
340
|
protocolVersion: AUDIT_PROTOCOL_VERSION,
|
|
322
|
-
sessionId:
|
|
341
|
+
sessionId:
|
|
342
|
+
typeof first.sessionId === 'string' ? first.sessionId : sessionId,
|
|
323
343
|
createdAt: typeof first.createdAt === 'string' ? first.createdAt : '',
|
|
324
344
|
};
|
|
325
345
|
expectedPrevHash = computeMetadataHash(metadata);
|
|
@@ -335,12 +355,16 @@ export function verifyAuditSessionChain(sessionId: string): AuditVerifyResult {
|
|
|
335
355
|
try {
|
|
336
356
|
parsed = JSON.parse(lines[i]) as WireRecord;
|
|
337
357
|
} catch (err) {
|
|
338
|
-
errors.push(
|
|
358
|
+
errors.push(
|
|
359
|
+
`Line ${lineNo}: invalid JSON (${err instanceof Error ? err.message : 'parse failure'}).`,
|
|
360
|
+
);
|
|
339
361
|
continue;
|
|
340
362
|
}
|
|
341
363
|
|
|
342
364
|
if (parsed.version !== AUDIT_PROTOCOL_VERSION) {
|
|
343
|
-
errors.push(
|
|
365
|
+
errors.push(
|
|
366
|
+
`Line ${lineNo}: unsupported version "${String(parsed.version)}".`,
|
|
367
|
+
);
|
|
344
368
|
continue;
|
|
345
369
|
}
|
|
346
370
|
if (!Number.isFinite(parsed.seq) || parsed.seq <= 0) {
|
|
@@ -348,7 +372,9 @@ export function verifyAuditSessionChain(sessionId: string): AuditVerifyResult {
|
|
|
348
372
|
continue;
|
|
349
373
|
}
|
|
350
374
|
if (parsed.seq !== expectedSeq) {
|
|
351
|
-
errors.push(
|
|
375
|
+
errors.push(
|
|
376
|
+
`Line ${lineNo}: expected seq ${expectedSeq}, got ${parsed.seq}.`,
|
|
377
|
+
);
|
|
352
378
|
}
|
|
353
379
|
if (parsed._prevHash !== expectedPrevHash) {
|
|
354
380
|
errors.push(`Line ${lineNo}: previous hash mismatch.`);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from 'crypto';
|
|
2
|
+
import type {
|
|
3
|
+
Attachment as DiscordAttachment,
|
|
4
|
+
Message as DiscordMessage,
|
|
5
|
+
} from 'discord.js';
|
|
1
6
|
import fs from 'fs';
|
|
2
7
|
import path from 'path';
|
|
3
|
-
import { randomUUID } from 'crypto';
|
|
4
|
-
|
|
5
|
-
import type { Attachment as DiscordAttachment, Message as DiscordMessage } from 'discord.js';
|
|
6
8
|
|
|
7
9
|
import { DATA_DIR } from '../../config.js';
|
|
8
10
|
import { logger } from '../../logger.js';
|
|
@@ -11,7 +13,9 @@ import type { MediaContextItem } from '../../types.js';
|
|
|
11
13
|
const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024;
|
|
12
14
|
const MAX_ATTACHMENT_CONTEXT_CHARS = 16_000;
|
|
13
15
|
const MAX_SINGLE_ATTACHMENT_CHARS = 8_000;
|
|
14
|
-
const DISCORD_MEDIA_CACHE_DIR = path.resolve(
|
|
16
|
+
const DISCORD_MEDIA_CACHE_DIR = path.resolve(
|
|
17
|
+
path.join(DATA_DIR, 'discord-media-cache'),
|
|
18
|
+
);
|
|
15
19
|
const CONTAINER_DISCORD_MEDIA_CACHE_DIR = '/discord-media-cache';
|
|
16
20
|
const DISCORD_ATTACHMENT_FETCH_TIMEOUT_MS = 12_000;
|
|
17
21
|
const DISCORD_CDN_HOST_PATTERNS: RegExp[] = [
|
|
@@ -26,10 +30,20 @@ export interface AttachmentContextResult {
|
|
|
26
30
|
media: MediaContextItem[];
|
|
27
31
|
}
|
|
28
32
|
|
|
29
|
-
export function looksLikeTextAttachment(
|
|
33
|
+
export function looksLikeTextAttachment(
|
|
34
|
+
name: string,
|
|
35
|
+
contentType: string,
|
|
36
|
+
): boolean {
|
|
30
37
|
if (contentType.startsWith('text/')) return true;
|
|
31
|
-
if (
|
|
32
|
-
|
|
38
|
+
if (
|
|
39
|
+
contentType.includes('json') ||
|
|
40
|
+
contentType.includes('xml') ||
|
|
41
|
+
contentType.includes('yaml')
|
|
42
|
+
)
|
|
43
|
+
return true;
|
|
44
|
+
return /\.(txt|md|markdown|json|ya?ml|js|jsx|ts|tsx|py|rb|go|rs|java|c|cpp|h|hpp|cs|php|html?|css|scss|sql|log|csv)$/i.test(
|
|
45
|
+
name,
|
|
46
|
+
);
|
|
33
47
|
}
|
|
34
48
|
|
|
35
49
|
function looksLikeImageAttachment(name: string, contentType: string): boolean {
|
|
@@ -38,14 +52,19 @@ function looksLikeImageAttachment(name: string, contentType: string): boolean {
|
|
|
38
52
|
}
|
|
39
53
|
|
|
40
54
|
function sanitizeAttachmentFilename(name: string): string {
|
|
41
|
-
const base = name
|
|
55
|
+
const base = name
|
|
56
|
+
.trim()
|
|
57
|
+
.replace(/[^\w.-]+/g, '-')
|
|
58
|
+
.replace(/-+/g, '-')
|
|
59
|
+
.replace(/^-+|-+$/g, '');
|
|
42
60
|
const bounded = base.slice(0, 96);
|
|
43
61
|
return bounded || 'attachment';
|
|
44
62
|
}
|
|
45
63
|
|
|
46
64
|
function normalizeAttachmentPathForContainer(hostPath: string): string | null {
|
|
47
65
|
const relative = path.relative(DISCORD_MEDIA_CACHE_DIR, hostPath);
|
|
48
|
-
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
66
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative))
|
|
67
|
+
return null;
|
|
49
68
|
return `${CONTAINER_DISCORD_MEDIA_CACHE_DIR}/${relative.replace(/\\/g, '/')}`;
|
|
50
69
|
}
|
|
51
70
|
|
|
@@ -57,10 +76,15 @@ function isAllowedDiscordAttachmentUrl(rawUrl: string): boolean {
|
|
|
57
76
|
return false;
|
|
58
77
|
}
|
|
59
78
|
if (parsed.protocol !== 'https:') return false;
|
|
60
|
-
return DISCORD_CDN_HOST_PATTERNS.some((pattern) =>
|
|
79
|
+
return DISCORD_CDN_HOST_PATTERNS.some((pattern) =>
|
|
80
|
+
pattern.test(parsed.hostname),
|
|
81
|
+
);
|
|
61
82
|
}
|
|
62
83
|
|
|
63
|
-
async function fetchAttachmentText(
|
|
84
|
+
async function fetchAttachmentText(
|
|
85
|
+
url: string,
|
|
86
|
+
maxChars: number,
|
|
87
|
+
): Promise<string | null> {
|
|
64
88
|
try {
|
|
65
89
|
const response = await fetch(url);
|
|
66
90
|
if (!response.ok) return null;
|
|
@@ -102,7 +126,10 @@ async function cacheDiscordImageAttachment(params: {
|
|
|
102
126
|
}
|
|
103
127
|
|
|
104
128
|
const controller = new AbortController();
|
|
105
|
-
const timer = setTimeout(
|
|
129
|
+
const timer = setTimeout(
|
|
130
|
+
() => controller.abort(),
|
|
131
|
+
DISCORD_ATTACHMENT_FETCH_TIMEOUT_MS,
|
|
132
|
+
);
|
|
106
133
|
try {
|
|
107
134
|
const response = await fetch(candidateUrl, { signal: controller.signal });
|
|
108
135
|
if (!response.ok) {
|
|
@@ -110,17 +137,27 @@ async function cacheDiscordImageAttachment(params: {
|
|
|
110
137
|
continue;
|
|
111
138
|
}
|
|
112
139
|
|
|
113
|
-
const resolvedMime = String(
|
|
140
|
+
const resolvedMime = String(
|
|
141
|
+
response.headers.get('content-type') || fallbackMimeType || '',
|
|
142
|
+
)
|
|
114
143
|
.split(';')[0]
|
|
115
144
|
.trim()
|
|
116
145
|
.toLowerCase();
|
|
117
146
|
if (!resolvedMime.startsWith('image/')) {
|
|
118
|
-
fetchErrors.push(
|
|
147
|
+
fetchErrors.push(
|
|
148
|
+
`invalid_type:${resolvedMime || 'unknown'}@${candidateUrl}`,
|
|
149
|
+
);
|
|
119
150
|
continue;
|
|
120
151
|
}
|
|
121
152
|
|
|
122
|
-
const contentLength = Number.parseInt(
|
|
123
|
-
|
|
153
|
+
const contentLength = Number.parseInt(
|
|
154
|
+
response.headers.get('content-length') || '',
|
|
155
|
+
10,
|
|
156
|
+
);
|
|
157
|
+
if (
|
|
158
|
+
Number.isFinite(contentLength) &&
|
|
159
|
+
contentLength > MAX_ATTACHMENT_BYTES
|
|
160
|
+
) {
|
|
124
161
|
fetchErrors.push(`too_large_header:${contentLength}@${candidateUrl}`);
|
|
125
162
|
continue;
|
|
126
163
|
}
|
|
@@ -155,18 +192,22 @@ async function cacheDiscordImageAttachment(params: {
|
|
|
155
192
|
}
|
|
156
193
|
}
|
|
157
194
|
|
|
158
|
-
const fallbackUrl =
|
|
159
|
-
||
|
|
160
|
-
||
|
|
195
|
+
const fallbackUrl =
|
|
196
|
+
sourceCandidates.find((url) => isAllowedDiscordAttachmentUrl(url)) ||
|
|
197
|
+
sourceCandidates[0] ||
|
|
198
|
+
'';
|
|
161
199
|
return {
|
|
162
200
|
path: null,
|
|
163
201
|
sourceUrl: fallbackUrl,
|
|
164
202
|
mimeType: fallbackMimeType,
|
|
165
|
-
cacheError:
|
|
203
|
+
cacheError:
|
|
204
|
+
fetchErrors.length > 0 ? fetchErrors.join(' | ') : 'cache_failed',
|
|
166
205
|
};
|
|
167
206
|
}
|
|
168
207
|
|
|
169
|
-
export async function buildAttachmentContext(
|
|
208
|
+
export async function buildAttachmentContext(
|
|
209
|
+
messages: DiscordMessage[],
|
|
210
|
+
): Promise<AttachmentContextResult> {
|
|
170
211
|
const lines: string[] = [];
|
|
171
212
|
const media: MediaContextItem[] = [];
|
|
172
213
|
let remainingChars = MAX_ATTACHMENT_CONTEXT_CHARS;
|
|
@@ -179,7 +220,9 @@ export async function buildAttachmentContext(messages: DiscordMessage[]): Promis
|
|
|
179
220
|
const size = attachment.size || 0;
|
|
180
221
|
const contentType = (attachment.contentType || '').toLowerCase();
|
|
181
222
|
if (size > MAX_ATTACHMENT_BYTES) {
|
|
182
|
-
lines.push(
|
|
223
|
+
lines.push(
|
|
224
|
+
`- ${name}: skipped (size ${size} bytes exceeds 10MB limit)`,
|
|
225
|
+
);
|
|
183
226
|
if (looksLikeImageAttachment(name, contentType)) {
|
|
184
227
|
mediaOrder += 1;
|
|
185
228
|
media.push({
|
|
@@ -220,7 +263,9 @@ export async function buildAttachmentContext(messages: DiscordMessage[]): Promis
|
|
|
220
263
|
filename: name,
|
|
221
264
|
});
|
|
222
265
|
if (cached.path) {
|
|
223
|
-
lines.push(
|
|
266
|
+
lines.push(
|
|
267
|
+
`- ${name}: image attachment cached (${size} bytes, ${cached.mimeType || contentType || 'unknown type'})`,
|
|
268
|
+
);
|
|
224
269
|
logger.info(
|
|
225
270
|
{
|
|
226
271
|
messageId: msg.id,
|
|
@@ -233,7 +278,9 @@ export async function buildAttachmentContext(messages: DiscordMessage[]): Promis
|
|
|
233
278
|
'Discord image attachment cached successfully',
|
|
234
279
|
);
|
|
235
280
|
} else {
|
|
236
|
-
lines.push(
|
|
281
|
+
lines.push(
|
|
282
|
+
`- ${name}: image attachment (cache failed, using URL fallback)`,
|
|
283
|
+
);
|
|
237
284
|
logger.warn(
|
|
238
285
|
{
|
|
239
286
|
messageId: msg.id,
|
|
@@ -250,7 +297,10 @@ export async function buildAttachmentContext(messages: DiscordMessage[]): Promis
|
|
|
250
297
|
}
|
|
251
298
|
|
|
252
299
|
if (looksLikeTextAttachment(name, contentType)) {
|
|
253
|
-
const maxChars = Math.min(
|
|
300
|
+
const maxChars = Math.min(
|
|
301
|
+
MAX_SINGLE_ATTACHMENT_CHARS,
|
|
302
|
+
Math.max(500, remainingChars),
|
|
303
|
+
);
|
|
254
304
|
const text = await fetchAttachmentText(attachment.url, maxChars);
|
|
255
305
|
if (!text) {
|
|
256
306
|
lines.push(`- ${name}: text attachment (failed to read content)`);
|
|
@@ -261,7 +311,9 @@ export async function buildAttachmentContext(messages: DiscordMessage[]): Promis
|
|
|
261
311
|
remainingChars -= block.length;
|
|
262
312
|
lines.push(block);
|
|
263
313
|
if (remainingChars <= 0) {
|
|
264
|
-
lines.push(
|
|
314
|
+
lines.push(
|
|
315
|
+
'- Additional attachment content omitted (context budget reached).',
|
|
316
|
+
);
|
|
265
317
|
return {
|
|
266
318
|
context: `[Attachments]\n${lines.join('\n')}\n\n`,
|
|
267
319
|
media,
|
|
@@ -270,7 +322,9 @@ export async function buildAttachmentContext(messages: DiscordMessage[]): Promis
|
|
|
270
322
|
continue;
|
|
271
323
|
}
|
|
272
324
|
|
|
273
|
-
lines.push(
|
|
325
|
+
lines.push(
|
|
326
|
+
`- ${name}: attachment (${size} bytes, ${contentType || 'unknown type'})`,
|
|
327
|
+
);
|
|
274
328
|
}
|
|
275
329
|
}
|
|
276
330
|
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const CONTROL_COMMAND_RE = /^\/(stop|pause|clear|cancel|resume)\b/i;
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_DEBOUNCE_MS = 2_500;
|
|
4
|
+
export const DEFAULT_DEBOUNCE_MAX_BUFFER = 5;
|
|
5
|
+
|
|
6
|
+
export function resolveInboundDebounceMs(
|
|
7
|
+
globalDebounceMs: number,
|
|
8
|
+
channelOverrideMs?: number,
|
|
9
|
+
): number {
|
|
10
|
+
const selected = channelOverrideMs ?? globalDebounceMs;
|
|
11
|
+
return Math.max(0, Math.floor(selected));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function shouldDebounceInbound(params: {
|
|
15
|
+
content: string;
|
|
16
|
+
hasAttachments: boolean;
|
|
17
|
+
isPrefixedCommand: boolean;
|
|
18
|
+
}): boolean {
|
|
19
|
+
const normalized = params.content.trim();
|
|
20
|
+
if (!normalized) return false;
|
|
21
|
+
if (params.hasAttachments) return false;
|
|
22
|
+
if (params.isPrefixedCommand) return false;
|
|
23
|
+
if (CONTROL_COMMAND_RE.test(normalized)) return false;
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type {
|
|
2
|
+
AttachmentBuilder,
|
|
3
|
+
ChatInputCommandInteraction,
|
|
4
|
+
Message as DiscordMessage,
|
|
5
|
+
} from 'discord.js';
|
|
2
6
|
|
|
3
7
|
import { chunkMessage } from '../../chunk.js';
|
|
4
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
getHumanDelayMs,
|
|
10
|
+
type HumanDelayConfig,
|
|
11
|
+
sleep,
|
|
12
|
+
} from './human-delay.js';
|
|
13
|
+
import { type MentionLookup, rewriteUserMentions } from './mentions.js';
|
|
5
14
|
|
|
6
|
-
export type DiscordRetryFn = <T>(
|
|
15
|
+
export type DiscordRetryFn = <T>(
|
|
16
|
+
label: string,
|
|
17
|
+
fn: () => Promise<T>,
|
|
18
|
+
) => Promise<T>;
|
|
7
19
|
|
|
8
20
|
export function buildResponseText(text: string, toolsUsed?: string[]): string {
|
|
9
21
|
let body = text;
|
|
@@ -27,12 +39,16 @@ export function prepareChunkedPayloads(
|
|
|
27
39
|
files?: AttachmentBuilder[],
|
|
28
40
|
mentionLookup?: MentionLookup,
|
|
29
41
|
): { content: string; files?: AttachmentBuilder[] }[] {
|
|
30
|
-
const prepared = mentionLookup
|
|
42
|
+
const prepared = mentionLookup
|
|
43
|
+
? rewriteUserMentions(text, mentionLookup)
|
|
44
|
+
: text;
|
|
31
45
|
const chunks = chunkMessage(prepared, { maxChars: 1_900, maxLines: 20 });
|
|
32
46
|
const safeChunks = chunks.length > 0 ? chunks : ['(no content)'];
|
|
33
47
|
return safeChunks.map((content, i) => ({
|
|
34
48
|
content,
|
|
35
|
-
...(i === safeChunks.length - 1 && files && files.length > 0
|
|
49
|
+
...(i === safeChunks.length - 1 && files && files.length > 0
|
|
50
|
+
? { files }
|
|
51
|
+
: {}),
|
|
36
52
|
}));
|
|
37
53
|
}
|
|
38
54
|
|
|
@@ -42,6 +58,7 @@ export async function sendChunkedReply(params: {
|
|
|
42
58
|
withRetry: DiscordRetryFn;
|
|
43
59
|
files?: AttachmentBuilder[];
|
|
44
60
|
mentionLookup?: MentionLookup;
|
|
61
|
+
humanDelay?: HumanDelayConfig;
|
|
45
62
|
}): Promise<void> {
|
|
46
63
|
const payloads = prepareChunkedPayloads(
|
|
47
64
|
params.text,
|
|
@@ -52,9 +69,20 @@ export async function sendChunkedReply(params: {
|
|
|
52
69
|
if (i === 0) {
|
|
53
70
|
await params.withRetry('reply', () => params.msg.reply(payloads[i]));
|
|
54
71
|
} else {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
72
|
+
const delayMs = getHumanDelayMs(params.humanDelay);
|
|
73
|
+
if (delayMs > 0) {
|
|
74
|
+
await sleep(delayMs);
|
|
75
|
+
}
|
|
76
|
+
await params.withRetry('send', () =>
|
|
77
|
+
(
|
|
78
|
+
params.msg.channel as unknown as {
|
|
79
|
+
send: (next: {
|
|
80
|
+
content: string;
|
|
81
|
+
files?: AttachmentBuilder[];
|
|
82
|
+
}) => Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
).send(payloads[i]),
|
|
85
|
+
);
|
|
58
86
|
}
|
|
59
87
|
}
|
|
60
88
|
}
|
|
@@ -65,14 +93,24 @@ export async function sendChunkedDirectReply(params: {
|
|
|
65
93
|
withRetry: DiscordRetryFn;
|
|
66
94
|
files?: AttachmentBuilder[];
|
|
67
95
|
mentionLookup?: MentionLookup;
|
|
96
|
+
humanDelay?: HumanDelayConfig;
|
|
68
97
|
}): Promise<void> {
|
|
69
98
|
const payloads = prepareChunkedPayloads(
|
|
70
99
|
params.text,
|
|
71
100
|
params.files,
|
|
72
101
|
params.mentionLookup,
|
|
73
102
|
);
|
|
74
|
-
const dm = await params.withRetry('dm-open', () =>
|
|
75
|
-
|
|
103
|
+
const dm = await params.withRetry('dm-open', () =>
|
|
104
|
+
params.msg.author.createDM(),
|
|
105
|
+
);
|
|
106
|
+
for (let i = 0; i < payloads.length; i += 1) {
|
|
107
|
+
if (i > 0) {
|
|
108
|
+
const delayMs = getHumanDelayMs(params.humanDelay);
|
|
109
|
+
if (delayMs > 0) {
|
|
110
|
+
await sleep(delayMs);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const payload = payloads[i];
|
|
76
114
|
await params.withRetry('dm-send', () => dm.send(payload));
|
|
77
115
|
}
|
|
78
116
|
}
|
|
@@ -88,12 +126,18 @@ export async function sendChunkedInteractionReply(params: {
|
|
|
88
126
|
const payload = { ...payloads[i], ephemeral: true };
|
|
89
127
|
if (i === 0) {
|
|
90
128
|
if (params.interaction.replied || params.interaction.deferred) {
|
|
91
|
-
await params.withRetry('interaction-followup', () =>
|
|
129
|
+
await params.withRetry('interaction-followup', () =>
|
|
130
|
+
params.interaction.followUp(payload),
|
|
131
|
+
);
|
|
92
132
|
} else {
|
|
93
|
-
await params.withRetry('interaction-reply', () =>
|
|
133
|
+
await params.withRetry('interaction-reply', () =>
|
|
134
|
+
params.interaction.reply(payload),
|
|
135
|
+
);
|
|
94
136
|
}
|
|
95
137
|
continue;
|
|
96
138
|
}
|
|
97
|
-
await params.withRetry('interaction-followup', () =>
|
|
139
|
+
await params.withRetry('interaction-followup', () =>
|
|
140
|
+
params.interaction.followUp(payload),
|
|
141
|
+
);
|
|
98
142
|
}
|
|
99
143
|
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface HumanDelayConfig {
|
|
2
|
+
mode: 'off' | 'natural' | 'custom';
|
|
3
|
+
minMs?: number;
|
|
4
|
+
maxMs?: number;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ResolvedHumanDelayConfig {
|
|
8
|
+
mode: 'off' | 'natural' | 'custom';
|
|
9
|
+
minMs: number;
|
|
10
|
+
maxMs: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_HUMAN_DELAY_MIN_MS = 800;
|
|
14
|
+
export const DEFAULT_HUMAN_DELAY_MAX_MS = 2_500;
|
|
15
|
+
|
|
16
|
+
export function resolveHumanDelayConfig(
|
|
17
|
+
config?: HumanDelayConfig,
|
|
18
|
+
): ResolvedHumanDelayConfig {
|
|
19
|
+
const mode = config?.mode ?? 'natural';
|
|
20
|
+
const minMs = Math.max(
|
|
21
|
+
0,
|
|
22
|
+
Math.floor(config?.minMs ?? DEFAULT_HUMAN_DELAY_MIN_MS),
|
|
23
|
+
);
|
|
24
|
+
const rawMax = Math.max(
|
|
25
|
+
0,
|
|
26
|
+
Math.floor(config?.maxMs ?? DEFAULT_HUMAN_DELAY_MAX_MS),
|
|
27
|
+
);
|
|
28
|
+
const maxMs = Math.max(minMs, rawMax);
|
|
29
|
+
return {
|
|
30
|
+
mode,
|
|
31
|
+
minMs,
|
|
32
|
+
maxMs,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getHumanDelayMs(config?: HumanDelayConfig): number {
|
|
37
|
+
const resolved = resolveHumanDelayConfig(config);
|
|
38
|
+
if (resolved.mode === 'off') return 0;
|
|
39
|
+
if (resolved.maxMs <= resolved.minMs) return resolved.minMs;
|
|
40
|
+
const span = resolved.maxMs - resolved.minMs + 1;
|
|
41
|
+
return resolved.minMs + Math.floor(Math.random() * span);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function sleep(ms: number): Promise<void> {
|
|
45
|
+
const waitMs = Math.max(0, Math.floor(ms));
|
|
46
|
+
if (waitMs <= 0) return;
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
48
|
+
}
|