openmausbot 0.1.68 → 0.1.69
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/assets/{index-HqooKFTS.js → index-BmHupgVJ.js} +1 -1
- package/dist/assets/index-COp5lMnv.js +279 -0
- package/dist/assets/index-Jk7TIeyW.css +1 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +38 -2
- package/dist-server/drivers/agents-proxy.js +85 -5
- package/dist-server/index.js +1465 -611
- package/dist-server/openmausbot.js +554 -191
- package/dist-server/pair-cli.js +554 -191
- package/dist-server/server/avatar-image.js +86 -34
- package/dist-server/server/bot-package.js +60 -14
- package/dist-server/server/config.js +23 -2
- package/dist-server/server/drivers/acp/qwen.js +142 -5
- package/dist-server/server/drivers/agents-proxy.js +95 -4
- package/dist-server/server/drivers/codex-device-auth.js +102 -3
- package/dist-server/server/drivers/codex-identity.js +115 -0
- package/dist-server/server/drivers/codex.js +6 -0
- package/dist-server/server/harness/registry.js +2 -0
- package/dist-server/server/index.js +26 -5
- package/dist-server/server/package-export.js +7 -0
- package/dist-server/server/provider-auth-sessions.js +24 -0
- package/dist-server/server/routine-requests.js +270 -25
- package/dist-server/server/routines.js +185 -18
- package/dist-server/server/steer-queue.js +28 -0
- package/dist-server/shared/image-generation.js +28 -0
- package/dist-server/vps-container-mcp.js +38 -2
- package/package.json +1 -1
- package/dist/assets/index-CbaejjQA.js +0 -278
- package/dist/assets/index-aJ5EWZOQ.css +0 -1
|
@@ -12,6 +12,9 @@ function routineRequestOwnerKey(owner) {
|
|
|
12
12
|
const ALL_DAYS = [0, 1, 2, 3, 4, 5, 6];
|
|
13
13
|
const CATCH_UP_MS = 12 * 60 * 60_000;
|
|
14
14
|
const MAX_DATE_MS = 8_640_000_000_000_000;
|
|
15
|
+
const LOCAL_DAY_MS = 24 * 60 * 60_000;
|
|
16
|
+
const INTERVAL_RESTRICTION_SEARCH_MS = 9 * LOCAL_DAY_MS;
|
|
17
|
+
const CLOCK_TIME = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
|
15
18
|
const MAX_RUNS = 2_000;
|
|
16
19
|
const MAX_ATTACHMENTS = 50;
|
|
17
20
|
const attachmentSchema = z.object({
|
|
@@ -86,7 +89,14 @@ function cloneSchedule(schedule) {
|
|
|
86
89
|
if (schedule.type === "once")
|
|
87
90
|
return { type: "once", at: schedule.at };
|
|
88
91
|
if (schedule.type === "interval") {
|
|
89
|
-
return {
|
|
92
|
+
return {
|
|
93
|
+
type: "interval",
|
|
94
|
+
everyMinutes: schedule.everyMinutes,
|
|
95
|
+
anchorAt: schedule.anchorAt,
|
|
96
|
+
...(schedule.weekdays ? { weekdays: [...schedule.weekdays] } : {}),
|
|
97
|
+
...(schedule.window ? { window: { ...schedule.window } } : {}),
|
|
98
|
+
...(schedule.endsAt === undefined ? {} : { endsAt: schedule.endsAt }),
|
|
99
|
+
};
|
|
90
100
|
}
|
|
91
101
|
return { type: "daily", time: schedule.time, weekdays: [...schedule.weekdays] };
|
|
92
102
|
}
|
|
@@ -164,6 +174,57 @@ function composeExecutionPrompt(prompt, attachments, carry) {
|
|
|
164
174
|
}
|
|
165
175
|
return parts.filter(Boolean).join("\n\n");
|
|
166
176
|
}
|
|
177
|
+
function cleanIntervalWeekdays(value) {
|
|
178
|
+
if (value == null)
|
|
179
|
+
return undefined;
|
|
180
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > ALL_DAYS.length) {
|
|
181
|
+
throw new Error("Choose at least one valid interval day");
|
|
182
|
+
}
|
|
183
|
+
if (value.some((day) => typeof day !== "number" || !Number.isInteger(day) || day < 0 || day > 6)) {
|
|
184
|
+
throw new Error("Interval days must be whole numbers from 0 to 6");
|
|
185
|
+
}
|
|
186
|
+
if (new Set(value).size !== value.length)
|
|
187
|
+
throw new Error("Choose each interval day only once");
|
|
188
|
+
const weekdays = [...value].sort((a, b) => a - b);
|
|
189
|
+
return weekdays.length === ALL_DAYS.length ? undefined : weekdays;
|
|
190
|
+
}
|
|
191
|
+
function clockMinutes(value) {
|
|
192
|
+
const match = CLOCK_TIME.exec(value);
|
|
193
|
+
return match ? Number(match[1]) * 60 + Number(match[2]) : null;
|
|
194
|
+
}
|
|
195
|
+
function cleanIntervalWindow(value, everyMinutes) {
|
|
196
|
+
if (value == null)
|
|
197
|
+
return undefined;
|
|
198
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
199
|
+
throw new Error("Choose a valid interval time window");
|
|
200
|
+
}
|
|
201
|
+
const candidate = value;
|
|
202
|
+
const start = typeof candidate.start === "string" ? candidate.start : "";
|
|
203
|
+
const end = typeof candidate.end === "string" ? candidate.end : "";
|
|
204
|
+
const startMinutes = clockMinutes(start);
|
|
205
|
+
const endMinutes = clockMinutes(end);
|
|
206
|
+
if (startMinutes == null || endMinutes == null) {
|
|
207
|
+
throw new Error("Interval window times must use HH:MM");
|
|
208
|
+
}
|
|
209
|
+
if (startMinutes >= endMinutes) {
|
|
210
|
+
throw new Error("Interval window must start before it ends on the same day");
|
|
211
|
+
}
|
|
212
|
+
if (endMinutes - startMinutes < everyMinutes) {
|
|
213
|
+
throw new Error("Interval window must be at least as long as the interval");
|
|
214
|
+
}
|
|
215
|
+
return { start, end };
|
|
216
|
+
}
|
|
217
|
+
function cleanIntervalEndsAt(value, anchorAt) {
|
|
218
|
+
if (value == null)
|
|
219
|
+
return undefined;
|
|
220
|
+
if (typeof value !== "number" ||
|
|
221
|
+
!Number.isSafeInteger(value) ||
|
|
222
|
+
value < anchorAt ||
|
|
223
|
+
value > MAX_DATE_MS) {
|
|
224
|
+
throw new Error("Choose a valid interval end time after its start");
|
|
225
|
+
}
|
|
226
|
+
return value;
|
|
227
|
+
}
|
|
167
228
|
function finishedOrder(run) {
|
|
168
229
|
return run.finishedAt ?? run.createdAt;
|
|
169
230
|
}
|
|
@@ -191,7 +252,17 @@ function cleanSchedule(schedule) {
|
|
|
191
252
|
anchorAt > MAX_DATE_MS) {
|
|
192
253
|
throw new Error("Choose a valid interval start time");
|
|
193
254
|
}
|
|
194
|
-
|
|
255
|
+
const weekdays = cleanIntervalWeekdays(schedule.weekdays);
|
|
256
|
+
const window = cleanIntervalWindow(schedule.window, everyMinutes);
|
|
257
|
+
const endsAt = cleanIntervalEndsAt(schedule.endsAt, anchorAt);
|
|
258
|
+
return {
|
|
259
|
+
type: "interval",
|
|
260
|
+
everyMinutes,
|
|
261
|
+
anchorAt,
|
|
262
|
+
...(weekdays ? { weekdays } : {}),
|
|
263
|
+
...(window ? { window } : {}),
|
|
264
|
+
...(endsAt === undefined ? {} : { endsAt }),
|
|
265
|
+
};
|
|
195
266
|
}
|
|
196
267
|
throw new Error("Choose a supported schedule");
|
|
197
268
|
}
|
|
@@ -203,17 +274,56 @@ function loadSchedule(value) {
|
|
|
203
274
|
return null;
|
|
204
275
|
}
|
|
205
276
|
}
|
|
277
|
+
function intervalHasRestrictions(schedule) {
|
|
278
|
+
return schedule.weekdays !== undefined || schedule.window !== undefined || schedule.endsAt !== undefined;
|
|
279
|
+
}
|
|
280
|
+
function intervalAllowsOccurrence(schedule, at) {
|
|
281
|
+
if (schedule.endsAt !== undefined && at > schedule.endsAt)
|
|
282
|
+
return false;
|
|
283
|
+
const date = new Date(at);
|
|
284
|
+
if (schedule.weekdays && !schedule.weekdays.includes(date.getDay()))
|
|
285
|
+
return false;
|
|
286
|
+
if (schedule.window) {
|
|
287
|
+
const minute = date.getHours() * 60 + date.getMinutes();
|
|
288
|
+
const start = clockMinutes(schedule.window.start);
|
|
289
|
+
const end = clockMinutes(schedule.window.end);
|
|
290
|
+
if (minute < start || minute >= end)
|
|
291
|
+
return false;
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
function isSameLocalDay(left, right) {
|
|
296
|
+
const a = new Date(left);
|
|
297
|
+
const b = new Date(right);
|
|
298
|
+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
|
|
299
|
+
}
|
|
300
|
+
function nextAlignedInterval(schedule, after) {
|
|
301
|
+
if (schedule.anchorAt > after)
|
|
302
|
+
return schedule.anchorAt;
|
|
303
|
+
const intervalMs = schedule.everyMinutes * 60_000;
|
|
304
|
+
const intervalsElapsed = Math.floor((after - schedule.anchorAt) / intervalMs);
|
|
305
|
+
const candidate = schedule.anchorAt + (intervalsElapsed + 1) * intervalMs;
|
|
306
|
+
return Number.isSafeInteger(candidate) && candidate <= MAX_DATE_MS ? candidate : null;
|
|
307
|
+
}
|
|
206
308
|
/** Next wall-clock occurrence in this computer's timezone, strictly after `after`. */
|
|
207
309
|
export function nextOccurrence(schedule, after) {
|
|
208
310
|
if (schedule.type === "once")
|
|
209
311
|
return schedule.at > after ? schedule.at : null;
|
|
210
312
|
if (schedule.type === "interval") {
|
|
211
|
-
if (schedule.anchorAt > after)
|
|
212
|
-
return schedule.anchorAt;
|
|
213
313
|
const intervalMs = schedule.everyMinutes * 60_000;
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
314
|
+
let candidate = nextAlignedInterval(schedule, after);
|
|
315
|
+
if (!intervalHasRestrictions(schedule))
|
|
316
|
+
return candidate;
|
|
317
|
+
const maxCandidates = Math.ceil(INTERVAL_RESTRICTION_SEARCH_MS / intervalMs) + 2;
|
|
318
|
+
for (let checked = 0; candidate !== null && checked < maxCandidates; checked++) {
|
|
319
|
+
if (schedule.endsAt !== undefined && candidate > schedule.endsAt)
|
|
320
|
+
return null;
|
|
321
|
+
if (intervalAllowsOccurrence(schedule, candidate))
|
|
322
|
+
return candidate;
|
|
323
|
+
const next = candidate + intervalMs;
|
|
324
|
+
candidate = Number.isSafeInteger(next) && next <= MAX_DATE_MS ? next : null;
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
217
327
|
}
|
|
218
328
|
const [hour, minute] = schedule.time.split(":").map(Number);
|
|
219
329
|
const weekdays = new Set(cleanDays(schedule.weekdays));
|
|
@@ -227,10 +337,38 @@ export function nextOccurrence(schedule, after) {
|
|
|
227
337
|
return null;
|
|
228
338
|
}
|
|
229
339
|
function latestIntervalOccurrence(schedule, at) {
|
|
230
|
-
if (schedule.anchorAt > at)
|
|
231
|
-
return null;
|
|
232
340
|
const intervalMs = schedule.everyMinutes * 60_000;
|
|
233
|
-
|
|
341
|
+
const ceiling = Math.min(at, schedule.endsAt ?? at);
|
|
342
|
+
if (schedule.anchorAt > ceiling)
|
|
343
|
+
return null;
|
|
344
|
+
let candidate = schedule.anchorAt + Math.floor((ceiling - schedule.anchorAt) / intervalMs) * intervalMs;
|
|
345
|
+
if (!intervalHasRestrictions(schedule))
|
|
346
|
+
return candidate;
|
|
347
|
+
const maxCandidates = Math.ceil(INTERVAL_RESTRICTION_SEARCH_MS / intervalMs) + 2;
|
|
348
|
+
for (let checked = 0; checked < maxCandidates; checked++) {
|
|
349
|
+
if (intervalAllowsOccurrence(schedule, candidate))
|
|
350
|
+
return candidate;
|
|
351
|
+
const previous = candidate - intervalMs;
|
|
352
|
+
if (!Number.isSafeInteger(previous) || previous < schedule.anchorAt)
|
|
353
|
+
return null;
|
|
354
|
+
candidate = previous;
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
function mergeScheduleUpdate(current, incoming) {
|
|
359
|
+
if (current.type !== "interval" || incoming.type !== "interval")
|
|
360
|
+
return incoming;
|
|
361
|
+
const merged = { ...incoming };
|
|
362
|
+
if (!Object.hasOwn(incoming, "weekdays") && current.weekdays) {
|
|
363
|
+
merged.weekdays = [...current.weekdays];
|
|
364
|
+
}
|
|
365
|
+
if (!Object.hasOwn(incoming, "window") && current.window) {
|
|
366
|
+
merged.window = { ...current.window };
|
|
367
|
+
}
|
|
368
|
+
if (!Object.hasOwn(incoming, "endsAt") && current.endsAt !== undefined) {
|
|
369
|
+
merged.endsAt = current.endsAt;
|
|
370
|
+
}
|
|
371
|
+
return merged;
|
|
234
372
|
}
|
|
235
373
|
function sanitizeInput(input) {
|
|
236
374
|
const name = String(input.name ?? "").trim().slice(0, 80);
|
|
@@ -467,13 +605,17 @@ export class RoutineManager {
|
|
|
467
605
|
if (this.targetState(clean) === "missing")
|
|
468
606
|
throw new Error(this.missingTargetMessage(clean.target));
|
|
469
607
|
const at = this.now();
|
|
608
|
+
const nextRunAt = clean.enabled ? this.initialOccurrence(clean.schedule, at) : null;
|
|
609
|
+
if (clean.schedule.type === "interval" && clean.enabled && nextRunAt === null) {
|
|
610
|
+
throw new Error("This interval has no future runs. Choose a later end date or turn it off.");
|
|
611
|
+
}
|
|
470
612
|
const routine = {
|
|
471
613
|
id: randomUUID(),
|
|
472
614
|
...clean,
|
|
473
615
|
// Only a confirmed chat card supplies `request`; the public calendar
|
|
474
616
|
// API cannot choose an arbitrary transcript as a reporting target.
|
|
475
617
|
sourceThreadId: request?.threadId,
|
|
476
|
-
nextRunAt
|
|
618
|
+
nextRunAt,
|
|
477
619
|
createdAt: at,
|
|
478
620
|
updatedAt: at,
|
|
479
621
|
};
|
|
@@ -505,7 +647,7 @@ export class RoutineManager {
|
|
|
505
647
|
groupId: Object.hasOwn(patch, "groupId") ? patch.groupId : routine.groupId,
|
|
506
648
|
runOn: patch.runOn ?? routine.runOn,
|
|
507
649
|
enabled: patch.enabled ?? routine.enabled,
|
|
508
|
-
schedule: patch.schedule
|
|
650
|
+
schedule: patch.schedule ? mergeScheduleUpdate(routine.schedule, patch.schedule) : routine.schedule,
|
|
509
651
|
durationMinutes: patch.durationMinutes ?? routine.durationMinutes,
|
|
510
652
|
timeoutMinutes: Object.hasOwn(patch, "timeoutMinutes") ? patch.timeoutMinutes : routine.timeoutMinutes,
|
|
511
653
|
attachments: patch.attachments ?? routine.attachments,
|
|
@@ -513,16 +655,19 @@ export class RoutineManager {
|
|
|
513
655
|
});
|
|
514
656
|
if (this.targetState(clean) === "missing")
|
|
515
657
|
throw new Error(this.missingTargetMessage(clean.target));
|
|
516
|
-
const cancelledRuns = [];
|
|
517
658
|
const scheduleChanged = JSON.stringify(clean.schedule) !== JSON.stringify(routine.schedule);
|
|
518
659
|
const enabledChanged = clean.enabled !== routine.enabled;
|
|
660
|
+
// Definition-only edits retain due work and offline catch-up.
|
|
661
|
+
const nextRunAt = !clean.enabled ? null : scheduleChanged || enabledChanged
|
|
662
|
+
? this.initialOccurrence(clean.schedule, now)
|
|
663
|
+
: routine.nextRunAt;
|
|
664
|
+
if (clean.schedule.type === "interval" && clean.enabled && nextRunAt === null) {
|
|
665
|
+
throw new Error("This interval has no future runs. Choose a later end date or turn it off.");
|
|
666
|
+
}
|
|
667
|
+
const cancelledRuns = [];
|
|
519
668
|
this.commitMutation(() => {
|
|
520
669
|
Object.assign(routine, clean, {
|
|
521
|
-
|
|
522
|
-
// became due since the last tick (or erase an offline catch-up).
|
|
523
|
-
nextRunAt: !clean.enabled ? null : scheduleChanged || enabledChanged
|
|
524
|
-
? this.initialOccurrence(clean.schedule, now)
|
|
525
|
-
: routine.nextRunAt,
|
|
670
|
+
nextRunAt,
|
|
526
671
|
// `updatedAt` doubles as the optimistic revision on durable routine
|
|
527
672
|
// confirmation cards. Keep it monotonic even for two writes in one ms.
|
|
528
673
|
updatedAt: Math.max(now, routine.updatedAt + 1),
|
|
@@ -871,6 +1016,10 @@ export class RoutineManager {
|
|
|
871
1016
|
routine.enabled = false;
|
|
872
1017
|
routine.updatedAt = Math.max(now, routine.updatedAt + 1);
|
|
873
1018
|
}
|
|
1019
|
+
else if (routine.schedule.type === "interval" && routine.nextRunAt === null) {
|
|
1020
|
+
routine.enabled = false;
|
|
1021
|
+
routine.updatedAt = Math.max(now, routine.updatedAt + 1);
|
|
1022
|
+
}
|
|
874
1023
|
this.emitRoutine(routine);
|
|
875
1024
|
changed = true;
|
|
876
1025
|
}
|
|
@@ -900,6 +1049,15 @@ export class RoutineManager {
|
|
|
900
1049
|
this.save();
|
|
901
1050
|
this.emitRun(run);
|
|
902
1051
|
}
|
|
1052
|
+
const requiresCurrentDayOccurrence = definition.schedule.weekdays !== undefined || definition.schedule.window !== undefined;
|
|
1053
|
+
const mayDispatch = intervalAllowsOccurrence(definition.schedule, now) &&
|
|
1054
|
+
(!requiresCurrentDayOccurrence || (latest !== null && isSameLocalDay(latest, now)));
|
|
1055
|
+
if (!mayDispatch) {
|
|
1056
|
+
if (nextOccurrence(definition.schedule, now) === null) {
|
|
1057
|
+
this.missQueuedRun(run, "The routine ended before this scheduled run could start");
|
|
1058
|
+
}
|
|
1059
|
+
continue;
|
|
1060
|
+
}
|
|
903
1061
|
}
|
|
904
1062
|
const state = this.targetState(run);
|
|
905
1063
|
if (state === "busy")
|
|
@@ -1071,6 +1229,15 @@ export class RoutineManager {
|
|
|
1071
1229
|
this.emitRun(run);
|
|
1072
1230
|
this.options.onRunFailed?.(cloneRun(run));
|
|
1073
1231
|
}
|
|
1232
|
+
missQueuedRun(run, message) {
|
|
1233
|
+
run.status = "missed";
|
|
1234
|
+
run.attention = undefined;
|
|
1235
|
+
run.error = redactSecretsInText(message).slice(0, 500);
|
|
1236
|
+
run.finishedAt = this.now();
|
|
1237
|
+
this.save();
|
|
1238
|
+
this.emitRun(run);
|
|
1239
|
+
this.options.onRunFailed?.(cloneRun(run));
|
|
1240
|
+
}
|
|
1074
1241
|
targetState(target) {
|
|
1075
1242
|
if (target.target === "room-goal") {
|
|
1076
1243
|
if (!target.groupId || !this.options.goalState)
|
|
@@ -18,6 +18,30 @@
|
|
|
18
18
|
// the feature.
|
|
19
19
|
import { newId } from "./contracts.js";
|
|
20
20
|
const queues = new Map(); // threadId → waiting sends
|
|
21
|
+
const listeners = new Set();
|
|
22
|
+
const changed = () => {
|
|
23
|
+
for (const listener of listeners) {
|
|
24
|
+
try {
|
|
25
|
+
listener();
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
console.warn("steer-queue: change listener failed");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
/** Public pending chips only: never expose provider prompts or reply context. */
|
|
33
|
+
export function queuedSteerSnapshot(ownsThread) {
|
|
34
|
+
return Object.fromEntries([...queues]
|
|
35
|
+
.filter(([threadId, entry]) => ownsThread(entry.botId, threadId))
|
|
36
|
+
.map(([threadId, entry]) => [threadId, entry.items.map((item) => ({
|
|
37
|
+
queueId: item.messageId, text: item.text, ...(item.reason ? { reason: item.reason } : {}),
|
|
38
|
+
}))]));
|
|
39
|
+
}
|
|
40
|
+
/** Publish changes synchronously so every client can restore/cancel the queue. */
|
|
41
|
+
export function onSteeredQueueChange(listener) {
|
|
42
|
+
listeners.add(listener);
|
|
43
|
+
return () => { listeners.delete(listener); };
|
|
44
|
+
}
|
|
21
45
|
/** Hold a mid-turn send off the transcript until drain. */
|
|
22
46
|
export function queueSteeredMessage(botId, threadId, text, options = {}) {
|
|
23
47
|
const id = newId();
|
|
@@ -35,6 +59,7 @@ export function queueSteeredMessage(botId, threadId, text, options = {}) {
|
|
|
35
59
|
reason: options.reason,
|
|
36
60
|
});
|
|
37
61
|
queues.set(threadId, entry);
|
|
62
|
+
changed();
|
|
38
63
|
return { id };
|
|
39
64
|
}
|
|
40
65
|
/** Drain every queue whose task is idle: append the held lines (leaf is now
|
|
@@ -53,6 +78,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
53
78
|
if (!bot) {
|
|
54
79
|
// the bot or task was deleted while messages waited
|
|
55
80
|
queues.delete(threadId);
|
|
81
|
+
changed();
|
|
56
82
|
continue;
|
|
57
83
|
}
|
|
58
84
|
if (bot.busy || isBlocked?.(entry.botId, threadId))
|
|
@@ -60,6 +86,7 @@ export function drainSteeredMessages(store, run, isBlocked) {
|
|
|
60
86
|
// committed to draining: the entry leaves the map before anything runs,
|
|
61
87
|
// so a settle racing another settle can never fire the same queue twice
|
|
62
88
|
queues.delete(threadId);
|
|
89
|
+
changed();
|
|
63
90
|
const appended = [];
|
|
64
91
|
for (const item of entry.items) {
|
|
65
92
|
// queueId is the pending-chip identity from the 202; append still
|
|
@@ -107,6 +134,7 @@ export function cancelSteeredMessage(botId, messageId, expectedThreadId) {
|
|
|
107
134
|
queues.delete(threadId);
|
|
108
135
|
else
|
|
109
136
|
queues.set(threadId, { botId: entry.botId, items });
|
|
137
|
+
changed();
|
|
110
138
|
return true;
|
|
111
139
|
}
|
|
112
140
|
return false;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** A deliberately configured Images API endpoint, not a provider-returned URL.
|
|
2
|
+
* Local HTTP is useful for self-hosted routers; internet endpoints require TLS. */
|
|
3
|
+
export function normalizeImageGenerationUrl(value) {
|
|
4
|
+
const message = "Use an HTTPS API base URL, or HTTP for a local router, without passwords, query parameters or fragments";
|
|
5
|
+
let url;
|
|
6
|
+
try {
|
|
7
|
+
url = new URL(value.trim());
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error(message);
|
|
11
|
+
}
|
|
12
|
+
if (value.length > 2048 || url.username || url.password || url.search || url.hash)
|
|
13
|
+
throw new Error(message);
|
|
14
|
+
const host = url.hostname.toLowerCase();
|
|
15
|
+
const ipv4 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
|
|
16
|
+
const local = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")
|
|
17
|
+
|| host === "host.docker.internal" || host === "host.containers.internal"
|
|
18
|
+
|| host === "[::1]" || /^\[f[cd][0-9a-f]{2}:/.test(host)
|
|
19
|
+
|| (ipv4 && (Number(ipv4[1]) === 127 || Number(ipv4[1]) === 10
|
|
20
|
+
|| (Number(ipv4[1]) === 192 && Number(ipv4[2]) === 168)
|
|
21
|
+
|| (Number(ipv4[1]) === 172 && Number(ipv4[2]) >= 16 && Number(ipv4[2]) <= 31)));
|
|
22
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && local))
|
|
23
|
+
throw new Error(message);
|
|
24
|
+
if (ipv4 && (Number(ipv4[1]) === 0 || (Number(ipv4[1]) === 169 && Number(ipv4[2]) === 254)))
|
|
25
|
+
throw new Error(message);
|
|
26
|
+
url.pathname = url.pathname.replace(/\/+$/, "").replace(/\/images\/generations$/, "");
|
|
27
|
+
return url.toString().replace(/\/+$/, "");
|
|
28
|
+
}
|
|
@@ -14933,6 +14933,25 @@ function date4(params) {
|
|
|
14933
14933
|
// node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/external.js
|
|
14934
14934
|
config(en_default());
|
|
14935
14935
|
|
|
14936
|
+
// shared/image-generation.ts
|
|
14937
|
+
function normalizeImageGenerationUrl(value) {
|
|
14938
|
+
const message = "Use an HTTPS API base URL, or HTTP for a local router, without passwords, query parameters or fragments";
|
|
14939
|
+
let url2;
|
|
14940
|
+
try {
|
|
14941
|
+
url2 = new URL(value.trim());
|
|
14942
|
+
} catch {
|
|
14943
|
+
throw new Error(message);
|
|
14944
|
+
}
|
|
14945
|
+
if (value.length > 2048 || url2.username || url2.password || url2.search || url2.hash) throw new Error(message);
|
|
14946
|
+
const host = url2.hostname.toLowerCase();
|
|
14947
|
+
const ipv43 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
|
|
14948
|
+
const local = host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local") || host === "host.docker.internal" || host === "host.containers.internal" || host === "[::1]" || /^\[f[cd][0-9a-f]{2}:/.test(host) || ipv43 && (Number(ipv43[1]) === 127 || Number(ipv43[1]) === 10 || Number(ipv43[1]) === 192 && Number(ipv43[2]) === 168 || Number(ipv43[1]) === 172 && Number(ipv43[2]) >= 16 && Number(ipv43[2]) <= 31);
|
|
14949
|
+
if (url2.protocol !== "https:" && !(url2.protocol === "http:" && local)) throw new Error(message);
|
|
14950
|
+
if (ipv43 && (Number(ipv43[1]) === 0 || Number(ipv43[1]) === 169 && Number(ipv43[2]) === 254)) throw new Error(message);
|
|
14951
|
+
url2.pathname = url2.pathname.replace(/\/+$/, "").replace(/\/images\/generations$/, "");
|
|
14952
|
+
return url2.toString().replace(/\/+$/, "");
|
|
14953
|
+
}
|
|
14954
|
+
|
|
14936
14955
|
// server/contracts.ts
|
|
14937
14956
|
var EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
|
|
14938
14957
|
|
|
@@ -15133,8 +15152,25 @@ var appConfigSchema = external_exports.object({
|
|
|
15133
15152
|
* engine: "elevenlabs" (default; needs a key) or "system" (the Mac's
|
|
15134
15153
|
* built-in voices, no key). */
|
|
15135
15154
|
tts: external_exports.object({ key: optionalText, voice: optionalText, provider: external_exports.enum(["elevenlabs", "system"]).optional() }).optional(),
|
|
15136
|
-
/**
|
|
15137
|
-
imageGen: external_exports.object({
|
|
15155
|
+
/** Avatar provider credentials stay separate; choosing a router never reuses a cloud key. */
|
|
15156
|
+
imageGen: external_exports.object({
|
|
15157
|
+
provider: external_exports.enum(["openai", "xai", "custom"]).optional(),
|
|
15158
|
+
key: optionalText,
|
|
15159
|
+
customApiKey: optionalText,
|
|
15160
|
+
customUrl: external_exports.string().trim().max(2048).transform((value, ctx) => {
|
|
15161
|
+
if (!value) return "";
|
|
15162
|
+
try {
|
|
15163
|
+
return normalizeImageGenerationUrl(value);
|
|
15164
|
+
} catch (error51) {
|
|
15165
|
+
ctx.addIssue({ code: "custom", message: error51.message });
|
|
15166
|
+
return external_exports.NEVER;
|
|
15167
|
+
}
|
|
15168
|
+
}).optional(),
|
|
15169
|
+
customModel: external_exports.string().trim().max(200).refine(
|
|
15170
|
+
(value) => !["\r", "\n", "\0"].some((character) => value.includes(character)),
|
|
15171
|
+
"Use a model ID without control characters"
|
|
15172
|
+
).optional()
|
|
15173
|
+
}).optional(),
|
|
15138
15174
|
/** Non-secret profile details shown in the sidebar. */
|
|
15139
15175
|
profile: external_exports.object({ name: optionalText, email: optionalText }).optional(),
|
|
15140
15176
|
/** UI language override (BCP-47, lowercase). Empty/absent = follow the
|