@poolzin/pool-bot 2026.3.21 → 2026.3.23
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/CHANGELOG.md +81 -0
- package/dist/acp/bindings-store.js +209 -0
- package/dist/acp/control-plane/runtime-cache.js +54 -0
- package/dist/acp/control-plane/runtime-options.js +215 -0
- package/dist/acp/control-plane/session-actor-queue.js +36 -0
- package/dist/acp/runtime/errors.js +47 -0
- package/dist/acp/runtime/registry.js +86 -0
- package/dist/acp/runtime/types.js +1 -0
- package/dist/acp/translator.js +97 -0
- package/dist/agents/failover-error.js +145 -47
- package/dist/browser/browser-profile-manager.js +319 -0
- package/dist/browser/cdp-proxy-bypass.js +129 -0
- package/dist/browser/cdp-timeouts.js +41 -0
- package/dist/browser/chrome-extension-validator.js +406 -0
- package/dist/browser/chrome-mcp-snapshot.js +222 -0
- package/dist/browser/chrome-mcp.js +421 -0
- package/dist/browser/chrome-mcp.snapshot.js +133 -0
- package/dist/browser/errors.js +67 -0
- package/dist/browser/form-fields.js +22 -0
- package/dist/browser/output-atomic.js +44 -0
- package/dist/browser/profile-capabilities.js +47 -0
- package/dist/browser/safe-filename.js +25 -0
- package/dist/browser/snapshot-roles.js +60 -0
- package/dist/build-info.json +3 -3
- package/dist/commands/security-owner-only.js +86 -0
- package/dist/control-ui/assets/{index-Dvkl4Xlx.js → index-D7shnQwQ.js} +404 -388
- package/dist/control-ui/assets/index-D7shnQwQ.js.map +1 -0
- package/dist/control-ui/index.html +1 -1
- package/dist/cron/cron-filters.js +150 -0
- package/dist/gateway/device-pairing-security.js +197 -0
- package/dist/gateway/event-deduplication.js +167 -0
- package/dist/gateway/run-tracker.js +253 -0
- package/dist/gateway/server-methods/nodes.js +14 -0
- package/dist/gateway/websocket-preauth-security.js +188 -0
- package/dist/infra/errors.js +53 -13
- package/dist/infra/exec-approvals-security.js +217 -0
- package/dist/infra/security/command-analyzer.js +257 -0
- package/dist/plugins/loader.js +16 -8
- package/dist/security/external-content.js +51 -1
- package/dist/sessions/session-costs.js +228 -0
- package/dist/shared/param-key.js +16 -0
- package/dist/shared/poll-params.js +58 -0
- package/dist/shared/polls.js +55 -0
- package/docs/DASHBOARD-GAP-ANALYSIS-AND-PLAN.md +430 -0
- package/docs/FEATURES.md +523 -0
- package/docs/FINAL-IMPLEMENTATION-REVIEW.md +274 -0
- package/docs/FINAL-IMPLEMENTATION-SUMMARY.md +356 -0
- package/docs/FINAL-PROFESSIONAL-EVALUATION.md +312 -0
- package/docs/IMPLEMENTATION-PRIORITY-EVALUATION.md +298 -0
- package/docs/IMPLEMENTATION-PROGRESS.md +237 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE1-2.md +381 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE4.md +389 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE5.md +420 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE6.md +422 -0
- package/docs/IMPLEMENTATION-REVIEW-PHASE7-FINAL.md +184 -0
- package/docs/MIKRODASH-ANALYSIS.md +412 -0
- package/docs/OPENCLAW-GAP-ANALYSIS-FINAL.md +431 -0
- package/docs/OPENCLAW-VS-POOLBOT-ANALYSIS.md +351 -0
- package/docs/PHASE-7-SUMMARY.md +144 -0
- package/docs/POOLBOT-OFFICE-PLAN.md +697 -0
- package/docs/PROJECT-FINAL-STATUS.md +237 -0
- package/docs/README.md +116 -0
- package/docs/REAL-IMPROVEMENTS-EVALUATION.md +477 -0
- package/docs/SECURITY-HARDENING-IMPLEMENTATION.md +161 -0
- package/docs/channels/googlechat.md +235 -206
- package/docs/channels/irc.md +332 -0
- package/docs/channels/nostr.md +255 -168
- package/docs/components/command-palette.md +166 -0
- package/docs/components/login-gate.md +219 -0
- package/docs/getting-started/installation.md +191 -0
- package/docs/getting-started/introduction.md +120 -0
- package/docs/improvements/USAGE-GUIDE.md +359 -0
- package/docs/plans/2026-03-15-openclaw-features-implementation.md +1632 -0
- package/docs/reference/deadcode-detection.md +72 -0
- package/extensions/acpx/node_modules/.bin/acpx +21 -0
- package/extensions/agency-agents/node_modules/.bin/vite +4 -4
- package/extensions/agency-agents/node_modules/.bin/vitest +2 -2
- package/extensions/googlechat/node_modules/.bin/tsc +21 -0
- package/extensions/googlechat/node_modules/.bin/tsserver +21 -0
- package/extensions/googlechat/node_modules/.bin/vitest +21 -0
- package/extensions/googlechat/package.json +11 -28
- package/extensions/googlechat/src/googlechat-channel.test.ts +60 -0
- package/extensions/googlechat/src/googlechat-channel.ts +120 -0
- package/extensions/googlechat/src/index.ts +14 -0
- package/extensions/irc/node_modules/.bin/tsc +21 -0
- package/extensions/irc/node_modules/.bin/tsserver +21 -0
- package/extensions/irc/node_modules/.bin/vitest +21 -0
- package/extensions/irc/package.json +16 -8
- package/extensions/irc/src/index.ts +14 -0
- package/extensions/irc/src/irc-channel.test.ts +43 -0
- package/extensions/irc/src/irc-channel.ts +191 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsc +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/tsserver +21 -0
- package/extensions/keyed-async-queue/node_modules/.bin/vitest +21 -0
- package/extensions/keyed-async-queue/package.json +20 -0
- package/extensions/keyed-async-queue/src/index.ts +14 -0
- package/extensions/keyed-async-queue/src/queue.test.ts +135 -0
- package/extensions/keyed-async-queue/src/queue.ts +200 -0
- package/extensions/memory-core/node_modules/.bin/tsc +21 -0
- package/extensions/memory-core/node_modules/.bin/tsserver +21 -0
- package/extensions/memory-core/node_modules/.bin/vitest +21 -0
- package/extensions/memory-core/package.json +11 -8
- package/extensions/memory-core/src/index.ts +14 -0
- package/extensions/memory-core/src/memory-manager.test.ts +124 -0
- package/extensions/memory-core/src/memory-manager.ts +186 -0
- package/extensions/nostr/node_modules/.bin/tsc +2 -2
- package/extensions/nostr/node_modules/.bin/tsserver +2 -2
- package/extensions/nostr/node_modules/.bin/vitest +21 -0
- package/extensions/nostr/package.json +15 -24
- package/extensions/nostr/src/index.ts +14 -0
- package/extensions/nostr/src/nostr-channel.test.ts +55 -0
- package/extensions/nostr/src/nostr-channel.ts +228 -0
- package/extensions/page-agent/node_modules/.bin/vitest +2 -2
- package/extensions/test-utils/node_modules/.bin/jiti +21 -0
- package/extensions/test-utils/node_modules/.bin/playwright +21 -0
- package/extensions/test-utils/node_modules/.bin/tsx +21 -0
- package/extensions/test-utils/node_modules/.bin/vite +21 -0
- package/extensions/test-utils/node_modules/.bin/vitest +21 -0
- package/extensions/test-utils/node_modules/.bin/yaml +21 -0
- package/extensions/xyops/node_modules/.bin/vitest +2 -2
- package/package.json +2 -1
- package/dist/control-ui/assets/index-Dvkl4Xlx.js.map +0 -1
- package/extensions/googlechat/node_modules/.bin/poolbot +0 -21
- package/extensions/memory-core/node_modules/.bin/poolbot +0 -21
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Cost Tracking
|
|
3
|
+
* Track token costs, time costs, and generate cost reports
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Calculate total cost from cost units
|
|
7
|
+
*/
|
|
8
|
+
export function calculateTotalCost(costs, rates) {
|
|
9
|
+
const defaultRates = {
|
|
10
|
+
tokens: 0.00002, // $0.00002 per token
|
|
11
|
+
time: 0.001, // $0.001 per second
|
|
12
|
+
api_calls: 0.001, // $0.001 per API call
|
|
13
|
+
custom: 0,
|
|
14
|
+
};
|
|
15
|
+
const effectiveRates = { ...defaultRates, ...rates };
|
|
16
|
+
return costs.reduce((total, cost) => {
|
|
17
|
+
const rate = effectiveRates[cost.type] || 0;
|
|
18
|
+
return total + cost.amount * rate;
|
|
19
|
+
}, 0);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Track session costs
|
|
23
|
+
*/
|
|
24
|
+
export class SessionCostTracker {
|
|
25
|
+
sessions = new Map();
|
|
26
|
+
rates = {};
|
|
27
|
+
constructor(rates) {
|
|
28
|
+
if (rates) {
|
|
29
|
+
this.rates = rates;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Start tracking a session
|
|
34
|
+
*/
|
|
35
|
+
startSession(sessionId, agentId, channelId) {
|
|
36
|
+
this.sessions.set(sessionId, {
|
|
37
|
+
sessionId,
|
|
38
|
+
agentId,
|
|
39
|
+
channelId,
|
|
40
|
+
costs: [],
|
|
41
|
+
startTime: Date.now(),
|
|
42
|
+
metadata: {},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Add cost to a session
|
|
47
|
+
*/
|
|
48
|
+
addCost(sessionId, cost) {
|
|
49
|
+
const session = this.sessions.get(sessionId);
|
|
50
|
+
if (!session) {
|
|
51
|
+
throw new Error(`Session ${sessionId} not found`);
|
|
52
|
+
}
|
|
53
|
+
session.costs.push(cost);
|
|
54
|
+
session.totalCost = calculateTotalCost(session.costs, this.rates);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Add token cost
|
|
58
|
+
*/
|
|
59
|
+
addTokenCost(sessionId, tokens, metadata) {
|
|
60
|
+
this.addCost(sessionId, {
|
|
61
|
+
type: "tokens",
|
|
62
|
+
amount: tokens,
|
|
63
|
+
metadata,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Add time cost
|
|
68
|
+
*/
|
|
69
|
+
addTimeCost(sessionId, seconds, metadata) {
|
|
70
|
+
this.addCost(sessionId, {
|
|
71
|
+
type: "time",
|
|
72
|
+
amount: seconds,
|
|
73
|
+
metadata,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Add API call cost
|
|
78
|
+
*/
|
|
79
|
+
addApiCallCost(sessionId, count = 1, metadata) {
|
|
80
|
+
this.addCost(sessionId, {
|
|
81
|
+
type: "api_calls",
|
|
82
|
+
amount: count,
|
|
83
|
+
metadata,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* End session tracking
|
|
88
|
+
*/
|
|
89
|
+
endSession(sessionId) {
|
|
90
|
+
const session = this.sessions.get(sessionId);
|
|
91
|
+
if (!session) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
session.endTime = Date.now();
|
|
95
|
+
// Add final time cost
|
|
96
|
+
const durationSeconds = (session.endTime - session.startTime) / 1000;
|
|
97
|
+
this.addTimeCost(sessionId, durationSeconds, { type: "session_duration" });
|
|
98
|
+
return session;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get session cost
|
|
102
|
+
*/
|
|
103
|
+
getSessionCost(sessionId) {
|
|
104
|
+
return this.sessions.get(sessionId);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get cost breakdown
|
|
108
|
+
*/
|
|
109
|
+
getCostBreakdown(sessionIds) {
|
|
110
|
+
const sessions = sessionIds
|
|
111
|
+
? sessionIds.map((id) => this.sessions.get(id)).filter((s) => !!s)
|
|
112
|
+
: Array.from(this.sessions.values());
|
|
113
|
+
const breakdown = {
|
|
114
|
+
byType: {},
|
|
115
|
+
byAgent: {},
|
|
116
|
+
byChannel: {},
|
|
117
|
+
bySession: {},
|
|
118
|
+
total: 0,
|
|
119
|
+
};
|
|
120
|
+
for (const session of sessions) {
|
|
121
|
+
const sessionTotal = calculateTotalCost(session.costs, this.rates);
|
|
122
|
+
// By type
|
|
123
|
+
for (const cost of session.costs) {
|
|
124
|
+
breakdown.byType[cost.type] =
|
|
125
|
+
(breakdown.byType[cost.type] || 0) + cost.amount * (this.rates[cost.type] || 0);
|
|
126
|
+
}
|
|
127
|
+
// By agent
|
|
128
|
+
if (session.agentId) {
|
|
129
|
+
breakdown.byAgent[session.agentId] =
|
|
130
|
+
(breakdown.byAgent[session.agentId] || 0) + sessionTotal;
|
|
131
|
+
}
|
|
132
|
+
// By channel
|
|
133
|
+
if (session.channelId) {
|
|
134
|
+
breakdown.byChannel[session.channelId] =
|
|
135
|
+
(breakdown.byChannel[session.channelId] || 0) + sessionTotal;
|
|
136
|
+
}
|
|
137
|
+
// By session
|
|
138
|
+
breakdown.bySession[session.sessionId] = sessionTotal;
|
|
139
|
+
// Total
|
|
140
|
+
breakdown.total += sessionTotal;
|
|
141
|
+
}
|
|
142
|
+
return breakdown;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Export costs to different formats
|
|
146
|
+
*/
|
|
147
|
+
exportCosts(format, sessionIds) {
|
|
148
|
+
const sessions = sessionIds
|
|
149
|
+
? sessionIds.map((id) => this.getSessionCost(id)).filter((s) => !!s)
|
|
150
|
+
: Array.from(this.sessions.values());
|
|
151
|
+
switch (format) {
|
|
152
|
+
case "csv":
|
|
153
|
+
return this.exportToCSV(sessions);
|
|
154
|
+
case "json":
|
|
155
|
+
return JSON.stringify(sessions, null, 2);
|
|
156
|
+
case "markdown":
|
|
157
|
+
return this.exportToMarkdown(sessions);
|
|
158
|
+
default:
|
|
159
|
+
throw new Error(`Unknown format: ${format}`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
exportToCSV(sessions) {
|
|
163
|
+
const lines = [
|
|
164
|
+
"Session ID,Agent ID,Channel ID,Start Time,End Time,Total Cost,Token Cost,Time Cost,API Calls",
|
|
165
|
+
];
|
|
166
|
+
for (const session of sessions) {
|
|
167
|
+
const tokenCost = session.costs
|
|
168
|
+
.filter((c) => c.type === "tokens")
|
|
169
|
+
.reduce((sum, c) => sum + c.amount, 0);
|
|
170
|
+
const timeCost = session.costs
|
|
171
|
+
.filter((c) => c.type === "time")
|
|
172
|
+
.reduce((sum, c) => sum + c.amount, 0);
|
|
173
|
+
const apiCalls = session.costs
|
|
174
|
+
.filter((c) => c.type === "api_calls")
|
|
175
|
+
.reduce((sum, c) => sum + c.amount, 0);
|
|
176
|
+
lines.push(`${session.sessionId},${session.agentId || ""},${session.channelId || ""},${session.startTime},${session.endTime || ""},${session.totalCost || 0},${tokenCost},${timeCost},${apiCalls}`);
|
|
177
|
+
}
|
|
178
|
+
return lines.join("\n");
|
|
179
|
+
}
|
|
180
|
+
exportToMarkdown(sessions) {
|
|
181
|
+
const lines = ["# Session Cost Report", ""];
|
|
182
|
+
const breakdown = this.getCostBreakdown(sessions.map((s) => s.sessionId));
|
|
183
|
+
lines.push("## Summary", "");
|
|
184
|
+
lines.push(`- **Total Cost**: $${breakdown.total.toFixed(4)}`);
|
|
185
|
+
lines.push(`- **Sessions**: ${sessions.length}`);
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("## By Type", "");
|
|
188
|
+
lines.push("| Type | Cost |");
|
|
189
|
+
lines.push("|------|------|");
|
|
190
|
+
for (const [type, cost] of Object.entries(breakdown.byType)) {
|
|
191
|
+
lines.push(`| ${type} | $${cost.toFixed(4)} |`);
|
|
192
|
+
}
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("## Sessions", "");
|
|
195
|
+
lines.push("| Session | Agent | Channel | Total Cost |");
|
|
196
|
+
lines.push("|---------|-------|---------|------------|");
|
|
197
|
+
for (const session of sessions) {
|
|
198
|
+
lines.push(`| ${session.sessionId} | ${session.agentId || "N/A"} | ${session.channelId || "N/A"} | $${(session.totalCost || 0).toFixed(4)} |`);
|
|
199
|
+
}
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Clear all sessions
|
|
204
|
+
*/
|
|
205
|
+
clear() {
|
|
206
|
+
this.sessions.clear();
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Get statistics
|
|
210
|
+
*/
|
|
211
|
+
getStats() {
|
|
212
|
+
const sessions = Array.from(this.sessions.values());
|
|
213
|
+
const _activeSessions = sessions.filter((s) => !s.endTime).length;
|
|
214
|
+
const _totalCost = sessions.reduce((sum, s) => sum + (s.totalCost || 0), 0);
|
|
215
|
+
return {
|
|
216
|
+
totalSessions: sessions.length,
|
|
217
|
+
activeSessions: _activeSessions,
|
|
218
|
+
totalCost: _totalCost,
|
|
219
|
+
avgCostPerSession: sessions.length > 0 ? _totalCost / sessions.length : 0,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Create default cost tracker
|
|
225
|
+
*/
|
|
226
|
+
export function createSessionCostTracker(rates) {
|
|
227
|
+
return new SessionCostTracker(rates);
|
|
228
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function toSnakeCaseKey(key) {
|
|
2
|
+
return key
|
|
3
|
+
.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2")
|
|
4
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1_$2")
|
|
5
|
+
.toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
export function readSnakeCaseParamRaw(params, key) {
|
|
8
|
+
if (Object.hasOwn(params, key)) {
|
|
9
|
+
return params[key];
|
|
10
|
+
}
|
|
11
|
+
const snakeKey = toSnakeCaseKey(key);
|
|
12
|
+
if (snakeKey !== key && Object.hasOwn(params, snakeKey)) {
|
|
13
|
+
return params[snakeKey];
|
|
14
|
+
}
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { readSnakeCaseParamRaw } from "./param-key.js";
|
|
2
|
+
export const POLL_CREATION_PARAM_DEFS = {
|
|
3
|
+
pollQuestion: { kind: "string" },
|
|
4
|
+
pollOption: { kind: "stringArray" },
|
|
5
|
+
pollDurationHours: { kind: "number" },
|
|
6
|
+
pollMulti: { kind: "boolean" },
|
|
7
|
+
pollDurationSeconds: { kind: "number", telegramOnly: true },
|
|
8
|
+
pollAnonymous: { kind: "boolean", telegramOnly: true },
|
|
9
|
+
pollPublic: { kind: "boolean", telegramOnly: true },
|
|
10
|
+
};
|
|
11
|
+
export const POLL_CREATION_PARAM_NAMES = Object.keys(POLL_CREATION_PARAM_DEFS);
|
|
12
|
+
function readPollParamRaw(params, key) {
|
|
13
|
+
return readSnakeCaseParamRaw(params, key);
|
|
14
|
+
}
|
|
15
|
+
export function resolveTelegramPollVisibility(params) {
|
|
16
|
+
if (params.pollAnonymous && params.pollPublic) {
|
|
17
|
+
throw new Error("pollAnonymous and pollPublic are mutually exclusive");
|
|
18
|
+
}
|
|
19
|
+
return params.pollAnonymous ? true : params.pollPublic ? false : undefined;
|
|
20
|
+
}
|
|
21
|
+
export function hasPollCreationParams(params) {
|
|
22
|
+
for (const key of POLL_CREATION_PARAM_NAMES) {
|
|
23
|
+
const def = POLL_CREATION_PARAM_DEFS[key];
|
|
24
|
+
const value = readPollParamRaw(params, key);
|
|
25
|
+
if (def.kind === "string" && typeof value === "string" && value.trim().length > 0) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
if (def.kind === "stringArray") {
|
|
29
|
+
if (Array.isArray(value) &&
|
|
30
|
+
value.some((entry) => typeof entry === "string" && entry.trim())) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (def.kind === "number") {
|
|
38
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
if (typeof value === "string") {
|
|
42
|
+
const trimmed = value.trim();
|
|
43
|
+
if (trimmed.length > 0 && Number.isFinite(Number(trimmed))) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (def.kind === "boolean") {
|
|
49
|
+
if (value === true) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === "string" && value.trim().toLowerCase() === "true") {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function resolvePollMaxSelections(optionCount, allowMultiselect) {
|
|
2
|
+
return allowMultiselect ? Math.max(2, optionCount) : 1;
|
|
3
|
+
}
|
|
4
|
+
export function normalizePollInput(input, options = {}) {
|
|
5
|
+
const question = input.question.trim();
|
|
6
|
+
if (!question) {
|
|
7
|
+
throw new Error("Poll question is required");
|
|
8
|
+
}
|
|
9
|
+
const pollOptions = (input.options ?? []).map((option) => option.trim());
|
|
10
|
+
const cleaned = pollOptions.filter(Boolean);
|
|
11
|
+
if (cleaned.length < 2) {
|
|
12
|
+
throw new Error("Poll requires at least 2 options");
|
|
13
|
+
}
|
|
14
|
+
if (options.maxOptions !== undefined && cleaned.length > options.maxOptions) {
|
|
15
|
+
throw new Error(`Poll supports at most ${options.maxOptions} options`);
|
|
16
|
+
}
|
|
17
|
+
const maxSelectionsRaw = input.maxSelections;
|
|
18
|
+
const maxSelections = typeof maxSelectionsRaw === "number" && Number.isFinite(maxSelectionsRaw)
|
|
19
|
+
? Math.floor(maxSelectionsRaw)
|
|
20
|
+
: 1;
|
|
21
|
+
if (maxSelections < 1) {
|
|
22
|
+
throw new Error("maxSelections must be at least 1");
|
|
23
|
+
}
|
|
24
|
+
if (maxSelections > cleaned.length) {
|
|
25
|
+
throw new Error("maxSelections cannot exceed option count");
|
|
26
|
+
}
|
|
27
|
+
const durationSecondsRaw = input.durationSeconds;
|
|
28
|
+
const durationSeconds = typeof durationSecondsRaw === "number" && Number.isFinite(durationSecondsRaw)
|
|
29
|
+
? Math.floor(durationSecondsRaw)
|
|
30
|
+
: undefined;
|
|
31
|
+
if (durationSeconds !== undefined && durationSeconds < 1) {
|
|
32
|
+
throw new Error("durationSeconds must be at least 1");
|
|
33
|
+
}
|
|
34
|
+
const durationRaw = input.durationHours;
|
|
35
|
+
const durationHours = typeof durationRaw === "number" && Number.isFinite(durationRaw)
|
|
36
|
+
? Math.floor(durationRaw)
|
|
37
|
+
: undefined;
|
|
38
|
+
if (durationHours !== undefined && durationHours < 1) {
|
|
39
|
+
throw new Error("durationHours must be at least 1");
|
|
40
|
+
}
|
|
41
|
+
if (durationSeconds !== undefined && durationHours !== undefined) {
|
|
42
|
+
throw new Error("durationSeconds and durationHours are mutually exclusive");
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
question,
|
|
46
|
+
options: cleaned,
|
|
47
|
+
maxSelections,
|
|
48
|
+
durationSeconds,
|
|
49
|
+
durationHours,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export function normalizePollDurationHours(value, options) {
|
|
53
|
+
const base = typeof value === "number" && Number.isFinite(value) ? Math.floor(value) : options.defaultHours;
|
|
54
|
+
return Math.min(Math.max(base, 1), options.maxHours);
|
|
55
|
+
}
|