mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(codeql): break remaining release alert flows (#4556)
This commit is contained in:
@@ -147,10 +147,44 @@ type ReplyMode = 'text' | 'voice';
|
||||
const AUTOCOMPLETE_POLL_DEBOUNCE_MS = 320;
|
||||
const AUTOCOMPLETE_MIN_CONTEXT_CHARS = 3;
|
||||
const debug = debugFactory('conversations');
|
||||
const SAFE_IMAGE_DATA_URI_RE = /^data:image\/(?:png|jpe?g|gif|webp|bmp);base64,[a-z0-9+/=\s]+$/i;
|
||||
const SAFE_IMAGE_DATA_URI_RE =
|
||||
/^data:(image\/(?:png|jpe?g|gif|webp|bmp));base64,([a-z0-9+/=\s]+)$/i;
|
||||
const EMPTY_IMAGE_SRC = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///ywAAAAAAQABAAACAUwAOw==';
|
||||
|
||||
function isSafeAttachmentImageSrc(src: string): boolean {
|
||||
return SAFE_IMAGE_DATA_URI_RE.test(src);
|
||||
function imageDataUriToObjectUrl(src: string): string | null {
|
||||
const match = SAFE_IMAGE_DATA_URI_RE.exec(src);
|
||||
if (!match) return null;
|
||||
try {
|
||||
const mime = match[1];
|
||||
const binary = atob(match[2].replace(/\s/g, ''));
|
||||
const bytes = new Uint8Array(binary.length);
|
||||
for (let i = 0; i < binary.length; i += 1) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
return URL.createObjectURL(new Blob([bytes], { type: mime }));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function AttachmentImage({ dataUri }: { dataUri: string }) {
|
||||
const [objectUrl, setObjectUrl] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const nextUrl = imageDataUriToObjectUrl(dataUri);
|
||||
setObjectUrl(nextUrl);
|
||||
return () => {
|
||||
if (nextUrl) URL.revokeObjectURL(nextUrl);
|
||||
};
|
||||
}, [dataUri]);
|
||||
|
||||
return (
|
||||
<img
|
||||
src={objectUrl ?? EMPTY_IMAGE_SRC}
|
||||
alt=""
|
||||
className="max-w-[200px] max-h-[200px] rounded-2xl object-cover"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConversationsProps {
|
||||
@@ -2330,7 +2364,7 @@ const Conversations = ({
|
||||
Array.isArray(msg.extraMetadata?.attachmentDataUris)
|
||||
? (msg.extraMetadata.attachmentDataUris as string[])
|
||||
: parsedContent.dataUris
|
||||
).filter(isSafeAttachmentImageSrc);
|
||||
).filter(src => SAFE_IMAGE_DATA_URI_RE.test(src));
|
||||
const hasImages = dataUris.length > 0;
|
||||
// Document attachments carry no image data-URI (only
|
||||
// images do); surface them as filename chips from the
|
||||
@@ -2362,12 +2396,7 @@ const Conversations = ({
|
||||
{hasImages && (
|
||||
<div className="flex flex-wrap gap-1.5 justify-end">
|
||||
{dataUris.map((uri, i) => (
|
||||
<img
|
||||
key={i}
|
||||
src={uri}
|
||||
alt=""
|
||||
className="max-w-[200px] max-h-[200px] rounded-2xl object-cover"
|
||||
/>
|
||||
<AttachmentImage key={i} dataUri={uri} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -9,8 +9,6 @@ import {
|
||||
import { buildDynamicCompletion } from "./llm/dynamic.mjs";
|
||||
import { headerValue, pickProbeText, resolveThreadKey } from "./llm/shared.mjs";
|
||||
|
||||
const MAX_STREAM_DELAY_MS = 30_000;
|
||||
|
||||
// The scripted `llmForcedResponses` FIFO models the *interactive* agent turn,
|
||||
// which always advertises tools (the orchestrator's delegate_* tools). Ancillary
|
||||
// completions that share the endpoint but carry no tools — thread-title/summary
|
||||
@@ -173,14 +171,37 @@ function writeSseEvent(res, payload) {
|
||||
res.write(`data: ${JSON.stringify(payload)}\n\n`);
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
function sleepDelay(ms) {
|
||||
switch (ms) {
|
||||
case 10:
|
||||
return new Promise((resolve) => setTimeout(resolve, 10));
|
||||
case 25:
|
||||
return new Promise((resolve) => setTimeout(resolve, 25));
|
||||
case 50:
|
||||
return new Promise((resolve) => setTimeout(resolve, 50));
|
||||
case 100:
|
||||
return new Promise((resolve) => setTimeout(resolve, 100));
|
||||
case 250:
|
||||
return new Promise((resolve) => setTimeout(resolve, 250));
|
||||
case 500:
|
||||
return new Promise((resolve) => setTimeout(resolve, 500));
|
||||
case 1000:
|
||||
return new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
default:
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
|
||||
function safeDelayMs(raw, fallback = 0) {
|
||||
const parsed = Number(raw);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return fallback;
|
||||
return Math.min(parsed, MAX_STREAM_DELAY_MS);
|
||||
if (parsed <= 10) return 10;
|
||||
if (parsed <= 25) return 25;
|
||||
if (parsed <= 50) return 50;
|
||||
if (parsed <= 100) return 100;
|
||||
if (parsed <= 250) return 250;
|
||||
if (parsed <= 500) return 500;
|
||||
return 1000;
|
||||
}
|
||||
|
||||
// Split a string into N-character windows so we can stream tool-call
|
||||
@@ -349,7 +370,7 @@ async function streamScriptToResponse({ res, model, script, defaultDelayMs }) {
|
||||
for (let i = 0; i < script.length; i += 1) {
|
||||
const entry = script[i] ?? {};
|
||||
const delay = safeDelayMs(entry.delayMs, defaultDelayMs);
|
||||
if (delay > 0) await sleep(delay);
|
||||
if (delay > 0) await sleepDelay(delay);
|
||||
|
||||
if (entry.error) {
|
||||
writeSseEvent(res, { error: { message: String(entry.error) } });
|
||||
@@ -406,7 +427,7 @@ async function streamScriptToResponse({ res, model, script, defaultDelayMs }) {
|
||||
}),
|
||||
);
|
||||
for (const piece of argPieces) {
|
||||
if (delay > 0) await sleep(delay);
|
||||
if (delay > 0) await sleepDelay(delay);
|
||||
writeSseEvent(
|
||||
res,
|
||||
sseChunkEnvelope({
|
||||
|
||||
Reference in New Issue
Block a user