mirror of
https://github.com/sune-org/sune.git
synced 2026-10-29 04:54:51 +00:00
Move reply time into a ts field and make reply ids base64url
Assistant messages now carry ts, a plain ms timestamp, instead of the time riding at the start of the id. Reply ids, which double as the proxy's run id, are just 128 random bits as unpadded base64url (22 chars). SECURITY.md now says ntfy pings carry a short hash of the run id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -8,4 +8,4 @@ Streaming is relayed through [`us.proxy.sune.chat`](https://github.com/sune-org/
|
||||
|
||||
**What it doesn't do:** no prompt logging (your messages are held in memory only while the run is in progress; the generated reply stays in memory for 90 minutes after it ends so you can reconnect, and nothing is written to disk), no reading your chats, no storing keys, no third-party sharing.
|
||||
|
||||
**What I see:** an [ntfy](https://ntfy.sh) ping when a run ends/fails, containing only: the run's start time (the timestamp half of its ID, not the secret half), `[provider/model]`, duration, error message (if any), and the sune's non-empty generation settings (temperature, top_p, top_k, frequency/repetition penalty, min_p, top_a, max tokens, reasoning effort, show thoughts, image output/aspect ratio/size). No prompts (system or master prompts included), no responses, no IP, no key. Source is public — audit it [here](https://github.com/sune-org/us.proxy.sune.chat).
|
||||
**What I see:** an [ntfy](https://ntfy.sh) ping when a run ends/fails, containing only: a short hash of the run's ID (not the ID itself), `[provider/model]`, duration, error message (if any), and the sune's non-empty generation settings (temperature, top_p, top_k, frequency/repetition penalty, min_p, top_a, max tokens, reasoning effort, show thoughts, image output/aspect ratio/size). No prompts (system or master prompts included), no responses, no IP, no key. Source is public — audit it [here](https://github.com/sune-org/us.proxy.sune.chat).
|
||||
|
||||
+2
-2
@@ -73,7 +73,7 @@ const renderUserUI=window.renderUserUI=()=>{if(!el.userMenuAvatar)return;const a
|
||||
const getSuneLabel=m=>{const name=(m&&m.sune_name)||SUNE.name,modelShort=getModelShort(m&&m.model);return `${name} · ${modelShort}`}
|
||||
function _createMessageRow(m){const role=typeof m==='string'?m:(m&&m.role)||'assistant',meta=typeof m==='string'?{}:m||{},isUser=role==='user',$row=$('<div class="flex flex-col gap-2"></div>'),$head=$('<div class="flex items-center gap-2 px-4"></div>'),$avatar=$('<div></div>');const uAva=isUser?USER.avatar:meta.avatar;uAva?$avatar.attr('class','msg-avatar shrink-0 h-7 w-7 rounded-full overflow-hidden').html(`<img src="${esc(uAva)}" class="h-full w-full object-cover">`):$avatar.attr('class',`${isUser?'bg-gray-900 text-white':'bg-gray-200 text-gray-900'} msg-avatar shrink-0 h-7 w-7 rounded-full flex items-center justify-center`).text(isUser?'👤':'✺');const $name=$('<div class="text-xs font-medium text-gray-500"></div>').text(isUser?USER.name:getSuneLabel(meta));const $deleteBtn=$('<button class="p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-red-500" title="Delete message"><i data-lucide="x" class="h-4 w-4"></i></button>').on('click',async e=>{e.stopPropagation();state.messages=state.messages.filter(msg=>msg.id!==m.id);$row.remove();dropReply(m.id);await THREAD.persist()});const $copyBtn=$('<button class="ml-auto p-1.5 rounded-lg hover:bg-gray-200 text-gray-400 hover:text-gray-600" title="Copy message"><i data-lucide="copy" class="h-4 w-4"></i></button>').on('click',async function(e){e.stopPropagation();const cur=state.messages.find(x=>x.id===m.id)||m;if(await copyToClipboard(partsToText(cur,true))){$(this).html('<i data-lucide="check" class="h-4 w-4 text-green-500"></i>');icons();setTimeout(()=>{$(this).html('<i data-lucide="copy" class="h-4 w-4"></i>');icons()},1200)}});$head.append($avatar,$name,$copyBtn,$deleteBtn);const $bubble=$(`<div class="${(isUser?'bg-gray-50 border border-gray-200':'bg-gray-100')+' msg-bubble markdown-body rounded-none px-4 py-3 w-full'}"></div>`);$row.append($head,$bubble);const vids=(Array.isArray(meta.content)?meta.content:[]).filter(p=>p?.type==='video_url');if(vids.length)$row.append(videoStrip(vids));return $row}
|
||||
function msgRow(m){const $row=_createMessageRow(m);$(el.messages).append($row);queueMicrotask(()=>{el.chat.scrollTo({top:el.chat.scrollHeight,behavior:'smooth'});icons()});return $row.find('.msg-bubble')[0]}
|
||||
const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m));if(track)state.messages.push(m);if(m.role==='assistant')el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:m}}));return bubble}
|
||||
const addMessage=window.addMessage=function(m,track=true){m.id=m.id||gid();if(m.role==='assistant')m.ts??=Date.now();if(!Array.isArray(m.content)&&m.content!=null){m.content=[{type:'text',text:String(m.content)}]}const bubble=msgRow(m);bubble.dataset.mid=m.id;renderMarkdown(bubble,partsToText(m));if(track)state.messages.push(m);if(m.role==='assistant')el.composer.dispatchEvent(new CustomEvent('sune:newSuneResponse',{detail:{message:m}}));return bubble}
|
||||
const addSuneBubbleStreaming=(meta,id)=>msgRow(Object.assign({role:'assistant',id},meta))
|
||||
const clearChat=()=>{el.suneHtml.dispatchEvent(new CustomEvent('sune:unmount'));state.messages=[];el.messages.innerHTML='';clearAttachments();revokeVideoUrls();syncBtn()}
|
||||
const payloadWithSampling=b=>{const o=Object.assign({},b),s=SUNE,p={temperature:num(s.temperature,null),top_p:num(s.top_p,null),top_k:int(s.top_k,null),frequency_penalty:num(s.frequency_penalty,null),repetition_penalty:num(s.repetition_penalty,null),min_p:num(s.min_p,null),top_a:num(s.top_a,null),max_tokens:int(s.max_tokens,null)>0?int(s.max_tokens,null):null};Object.keys(p).forEach(k=>{const v=p[k];if(v!==null)o[k]=v});return o}
|
||||
@@ -138,7 +138,7 @@ $(el.composer).on('paste',async e=>{
|
||||
addAttachments(files);
|
||||
})
|
||||
// Shared by send and SUNE.infer: adds the assistant bubble and starts its stream
|
||||
function reply(){const a=SUNE.active,meta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},id=sid(),msg=Object.assign({id,role:'assistant',content:[{type:'text',text:''}]},meta,{stream:{seq:-1}}),b=addSuneBubbleStreaming(meta,id);b.dataset.mid=id;b.innerHTML=SUNE_LOGO_SVG;state.messages.push(msg);THREAD.persist(false);startReply(msg)}
|
||||
function reply(){const a=SUNE.active,meta={sune_name:a.name,model:SUNE.model,avatar:a.avatar||''},id=sid(),msg=Object.assign({id,role:'assistant',ts:Date.now(),content:[{type:'text',text:''}]},meta,{stream:{seq:-1}}),b=addSuneBubbleStreaming(meta,id);b.dataset.mid=id;b.innerHTML=SUNE_LOGO_SVG;state.messages.push(msg);THREAD.persist(false);startReply(msg)}
|
||||
// The lock is taken before any await so a double tap can't send twice; a tap on the button just after it flipped from Stop is ignored
|
||||
$(el.composer).on('submit',async e=>{e.preventDefault();if(state.busy||sending||(e.submitter===el.sendBtn&&Date.now()-btnSwapAt<400))return;state.abortRequested=false;sending=true;try{await attachQueue;const text=el.input.value.trim();if(!text&&!state.attachments.length){sending=false;return await SUNE.infer()}el.input.value='';const parts=[];if(text)parts.push({type:'text',text});parts.push(...state.attachments);state.attachments=[];updateAttachBadge();el.fileInput.value='';await ensureThreadOnFirstUser(text||'(attachments)');const th=THREAD.active,shouldGenTitle=th&&!th.title;const userMsg={role:'user',content:parts.length?parts:[{type:'text',text:text||'(sent attachments)'}]};addMessage(userMsg);el.composer.dispatchEvent(new CustomEvent('user:send',{detail:{message:userMsg}}));if(shouldGenTitle)(async()=>{const title=await generateTitleWithAI(state.messages)||partsToText(state.messages.find(m=>m.role==='user')).replace(/!\[\]\(data:[^\)]+\)/g,'[Image]')||'Untitled';await THREAD.setTitle(th.id,title)})();if(SUNE.model)reply()}finally{sending=false}})
|
||||
let jars={html:null,extension:null};
|
||||
|
||||
+2
-2
@@ -8,8 +8,8 @@ export const positionPopover = (a, p) => {
|
||||
p.style.top = `${r.bottom + p.offsetHeight + 4 > window.innerHeight ? r.top - p.offsetHeight - 4 : r.bottom + 4}px`;
|
||||
p.style.left = `${Math.max(8, Math.min(r.right - p.offsetWidth, window.innerWidth - p.offsetWidth - 8))}px`;
|
||||
};
|
||||
// Reply ids double as the proxy's run id, so after the ms timestamp come 128 random bits
|
||||
export const sid = () => Date.now().toString(36) + '-' + [...crypto.getRandomValues(new Uint8Array(16))].map(b => b.toString(16).padStart(2, '0')).join('');
|
||||
// Reply ids double as the proxy's run id, so they're 128 random bits as unpadded base64url
|
||||
export const sid = () => btoa(String.fromCharCode(...crypto.getRandomValues(new Uint8Array(16)))).replace(/[+/=]/g, c => ({ '+': '-', '/': '_' })[c] || '');
|
||||
export const fmtSize = b => {
|
||||
const u = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
let i = 0, x = b;
|
||||
|
||||
Reference in New Issue
Block a user