// Standalone settings panel, using the existing authenticated admin API. const host = document.createElement('div'); host.id = 'mail-flow-tools'; const style = document.createElement('style'); style.textContent = `#mail-flow-tools .entry{position:fixed;right:24px;bottom:72px;z-index:40;padding:9px 14px;border:1px solid #cbd5e1;border-radius:8px;background:#fff;color:#0f172a;cursor:pointer}#mail-flow-dialog{width:min(760px,calc(100vw - 40px));max-height:85vh;overflow:auto;border:1px solid #cbd5e1;border-radius:12px;padding:24px;background:#fff;color:#0f172a}#mail-flow-dialog::backdrop{background:#0007}#mail-flow-dialog fieldset{border:1px solid #e2e8f0;border-radius:8px;margin:14px 0;padding:12px}#mail-flow-dialog label{display:block;margin:10px 0;font-size:14px}#mail-flow-dialog select,#mail-flow-dialog input:not([type=checkbox]),#mail-flow-dialog textarea{display:block;width:100%;padding:7px;border:1px solid #cbd5e1;border-radius:5px;box-sizing:border-box}#mail-flow-dialog button{padding:8px 14px;border:1px solid #cbd5e1;border-radius:6px;cursor:pointer}#mail-flow-dialog pre{white-space:pre-wrap;font-size:12px}#mail-flow-dialog p{font-size:13px;line-height:1.6}`; host.append(style); const entry = document.createElement('button'); entry.className='entry'; entry.textContent='邮件渠道与自动回复'; entry.type='button'; entry.hidden=true; host.append(entry); const dialog=document.createElement('dialog'); dialog.id='mail-flow-dialog'; host.append(dialog); document.body.append(host); const visibility=()=>{entry.hidden=!location.hash.startsWith('#/config') || !document.querySelector('#root h2'); if(entry.hidden && dialog.open) dialog.close();}; window.addEventListener('hashchange',visibility); new MutationObserver(visibility).observe(document.getElementById('root'),{childList:true,subtree:true}); visibility(); async function api(action, payload) { let token; try { token=JSON.parse(localStorage.getItem('XBOARD_ACCESS_TOKEN')||'null'); } catch { throw new Error('请重新登录后台'); } if(!token?.value || (token.expire!=null && token.expire<=Date.now())) throw new Error('请重新登录后台'); const base=new URL(window.settings?.base_url||'/',location.origin); if(base.origin!==location.origin) throw new Error('后台接口必须同源'); const url=base.pathname.replace(/\/$/,'')+'/api/v2/'+encodeURIComponent(window.settings.secure_path)+'/config/'+action; const response=await fetch(url,{method:payload?'POST':'GET',credentials:'same-origin',headers:{Authorization:token.value,'Content-Type':'application/json','Content-Language':'zh-CN'},...(payload?{body:JSON.stringify(payload)}:{})}); const data=await response.json(); if(!response.ok || data.status==='fail' || data.data===false) throw new Error(data.message||'请求失败,请检查输入'); return data.data; } const groups=[ ['发送渠道', [['enabled','启用邮件记录与分流','checkbox'],['resend_enabled','验证码、登录链接和工单通知优先 Resend','checkbox'],['resend_key','Resend API 密钥','password'],['resend_from','Resend 发件地址','email'],['resend_webhook_secret','Resend 回调签名密钥','password'],['reply_to','统一回复地址(常规邮箱)','email'],['daily_limit','日额度上限','number'],['monthly_limit','月额度上限','number']]], ['常规邮箱收件与自动回复', [['inbound_enabled','启用收件','checkbox'],['imap_host','IMAP 主机','text'],['imap_port','IMAP TLS 端口','number'],['imap_username','IMAP 用户名','text'],['imap_password','IMAP 密码','password'],['imap_folder','收件文件夹','text'],['auto_reply_enabled','自动确认收件(按发件地址限频)','checkbox'],['auto_reply_subject','自动回复标题','text'],['auto_reply_text','自动回复内容','textarea']]], ['每天八点汇报', [['report_enabled','启用 master 汇报','checkbox'],['master_url','Master 基础地址(HTTPS)','url'],['master_bot_code','Master 机器人代号','text'],['master_chat_code','Master 接收会话代号','text']]], ]; entry.addEventListener('click',async()=>{ dialog.textContent='正在读取…'; dialog.showModal(); entry.disabled=true; try { const data=await api('mailFlowStatus'); dialog.replaceChildren(); const heading=document.createElement('h2'); heading.textContent='邮件渠道与自动回复'; dialog.append(heading); const note=document.createElement('p'); note.textContent='群发、流量和到期提醒仍用原常规邮箱。Resend 额度不足时回退;收件固定用常规邮箱。密钥留空保留。自动回复仅处理启用之后的新邮件,并限制每个发件地址北京时间每天一次,无需绑定账户。'; dialog.append(note); const readiness=document.createElement('p'); readiness.textContent=(data.database_ready?'数据库迁移已就绪。':'数据库尚未迁移,暂不可启用。')+(data.imap_available?' IMAP 扩展可用。':' 收件需要安装 PHP IMAP 扩展。'); dialog.append(readiness); const form=document.createElement('form'); dialog.append(form); const inputs={}; const tierLabel=document.createElement('label'); tierLabel.textContent='自动邮件档位'; const tier=document.createElement('select'); tier.name='automatic_mail_level'; for(const [value,label] of [[1,'一档:保持原有提醒'],[2,'二档:仅套餐到期时提醒'],[3,'三档:关闭自动批量提醒']]) { const option=document.createElement('option'); option.value=String(value); option.textContent=label; tier.append(option); } if(data.automatic_mail_level==null) throw new Error('自动邮件档位暂不可读取,请稍后重试'); tier.value=String(data.automatic_mail_level); tierLabel.append(tier); form.append(tierLabel); const tierNote=document.createElement('p'); tierNote.textContent='人工群发、验证码、登录、工单回复和收件自动回复均不受档位影响。二档保留原提醒开关与用户到期提醒偏好,从启用二档之后的新到期开始,每分钟检查,不补发此前历史记录;改档取消禁发任务,恢复档位不补发已取消邮件。'; form.append(tierNote); for(const [title, fields] of groups){ const fieldset=document.createElement('fieldset'); const legend=document.createElement('legend'); legend.textContent=title; fieldset.append(legend); for(const [key,label,type] of fields){ const wrap=document.createElement('label'); wrap.textContent=label; const input=document.createElement(type==='textarea'?'textarea':'input'); if(type!=='textarea') input.type=type; if(type==='checkbox') input.checked=!!data.settings[key]; else if(type==='password') {input.autocomplete='new-password'; input.placeholder=data.settings[key+'_configured']?'已配置,留空保留':'尚未配置';} else input.value=data.settings[key]??''; if(type==='number'){input.step='1';input.min='1';if(key==='daily_limit') input.max='100';if(key==='monthly_limit') input.max='3000';if(key==='imap_port') input.max='65535';} inputs[key]=input; wrap.append(input); fieldset.append(wrap); } form.append(fieldset); } const message=document.createElement('p'); message.setAttribute('role','status'); form.append(message); const save=document.createElement('button'); save.type='submit'; save.textContent='保存'; form.append(save); const close=document.createElement('button'); close.type='button'; close.textContent='关闭';close.addEventListener('click',()=>dialog.close());form.append(close); const history=document.createElement('pre');history.textContent='最近邮件:\n'+data.messages.map(m=>`${m.created_at} · ${m.purpose} · ${m.channel||'待选择'} · ${m.status}${m.fallback_reason?' · '+m.fallback_reason:''}`).join('\n');dialog.append(history); for(const incoming of data.messages.filter(m=>m.direction==='in')) { const view=document.createElement('button'); view.type='button'; view.textContent='查看来信:'+incoming.email; const content=document.createElement('pre'); dialog.append(view,content); view.addEventListener('click',async()=>{view.disabled=true;try{const mail=await api('mailFlowIncoming?id='+encodeURIComponent(incoming.id));content.textContent=mail.subject+'\n\n'+mail.text;}catch(error){content.textContent=error.message;}finally{view.disabled=false;}}); } const reports=document.createElement('pre'); reports.textContent='最近汇报:\n'+data.reports.map(r=>r.report_date+' · '+r.status).join('\n');dialog.append(reports); form.addEventListener('submit',async event=>{ event.preventDefault();if(!form.reportValidity()) return; if((inputs.enabled.checked || Number(tier.value)===2) && !data.database_ready){message.textContent='请先执行数据库迁移。';return;} if(inputs.inbound_enabled.checked && !data.imap_available){message.textContent='请先安装 PHP IMAP 扩展。';return;} const values={};for(const [key,input] of Object.entries(inputs)){ if(input.type==='checkbox') values[key]=input.checked; else if(input.type==='password' && !input.value) continue; else values[key]=input.type==='number'?Number(input.value):input.value; } save.disabled=true;try{await api('save',{mail_flow:values,automatic_mail_level:Number(tier.value)});message.textContent='配置已保存。实际收发结果请查看邮件状态。';}catch(error){message.textContent=error.message;}finally{save.disabled=false;} }); } catch(error) {dialog.textContent=error.message;const close=document.createElement('button');close.textContent='关闭';close.addEventListener('click',()=>dialog.close());dialog.append(close);} finally {entry.disabled=false;} });