diff --git a/static/index.html b/static/index.html index 6ca13188..bf56e9ef 100644 --- a/static/index.html +++ b/static/index.html @@ -160,6 +160,7 @@ +
@@ -179,6 +180,7 @@ + @@ -340,6 +342,20 @@ + +
+
+ 自媒体中心 +
+
快捷操作
+
+ + + + + +
+
@@ -930,6 +946,15 @@
+ +
+
+
自媒体中心
+
+
+
+
+
diff --git a/static/panels.js b/static/panels.js index 93f28af5..f701cb15 100644 --- a/static/panels.js +++ b/static/panels.js @@ -45,7 +45,7 @@ const APP_TITLEBAR_KEYS = { memory: 'tab_memory', workspaces: 'tab_workspaces', profiles: 'tab_profiles', todos: 'tab_todos', insights: 'tab_insights', logs: 'tab_logs', settings: 'tab_settings', }; -const MAIN_VIEW_PANELS = ['settings','skills','memory','tasks','kanban','workspaces','profiles','insights','logs','plugin']; +const MAIN_VIEW_PANELS = ['settings','skills','memory','tasks','kanban','workspaces','profiles','insights','logs','plugin','selfmedia']; const MAIN_VIEW_SIDEBAR_PANEL_FALLBACKS = { plugin: 'settings' }; /** @@ -460,6 +460,7 @@ async function switchPanel(name, opts = {}) { if (nextPanel === 'todos') loadTodos(); if (nextPanel === 'insights') await loadInsights(); if (nextPanel === 'logs') await loadLogs(); + if (nextPanel === 'selfmedia') await loadSelfMediaPanel(); _syncLogsAutoRefresh(); if (typeof _syncSystemHealthMonitorVisibility === 'function') _syncSystemHealthMonitorVisibility(); if (nextPanel === 'settings') { @@ -11026,6 +11027,298 @@ async function _loadPluginPage(path, label) { _currentPluginPage = { path, label }; } +// ── Self-Media Center panel ────────────────────────────────────────────────── +// Native WebUI panel (no iframe): runs in the app origin so api() carries the +// session cookie + CSRF token. Quick actions open a fresh conversation and send +// the command there instead of hijacking the current chat. + +const SM_KEYWORDS = [ + '公众号','wechat','weixin','文章','publish','发文', + '自媒体','content','选题','写作','writing','article', + '头条','toutiao','b站','bilibili','bili','值得买','smzdm', + '知乎','zhihu','小红书','xhs','redbook', + 'post','social','media','推广','营销','运营', + '自动','daily','日报','周报','ranking','排行', + '视频','video','图文','图片','image','cover','封面', + '评论','comment','回复','reply', + 'seo','关键词','keyword','软件','app', + 'wechat-oa','weixin-','douyin','multi-platform','cross-post', + 'aws-wechat','aws-' +]; + +function _smIsSelfMedia(job){ + if(!job||!job.name) return false; + const n=String(job.name).toLowerCase(); + return SM_KEYWORDS.some(k=>n.includes(k.toLowerCase())); +} + +function _smAgo(t){ + if(!t) return ''; + const d=t instanceof Date?t:new Date(t); + if(isNaN(d.getTime())) return ''; + const mins=Math.floor((Date.now()-d.getTime())/60000); + if(mins<1) return '刚刚'; + if(mins<60) return mins+'分钟前'; + const h=Math.floor(mins/60); + if(h<24) return h+'小时前'; + const days=Math.floor(h/24); + if(days<7) return days+'天前'; + return d.toLocaleDateString('zh-CN',{month:'short',day:'numeric'}); +} + +function _smEsc(s){ + const d=document.createElement('div'); + d.textContent=s==null?'':String(s); + return d.innerHTML; +} + +// Open a brand-new conversation and send the command there. +let _smLaunchInFlight=false; +async function openSelfMediaCommand(command){ + if(!command) return; + if(_smLaunchInFlight){ if(typeof showToast==='function') showToast('上一个操作还在启动中…',1800); return; } + _smLaunchInFlight=true; + try{ + if(typeof newSession==='function') await newSession(); + if(typeof switchPanel==='function') switchPanel('chat'); + const box=document.getElementById('msg'); + if(box){ + box.value=command; + box.dispatchEvent(new Event('input',{bubbles:true})); + box.focus(); + } + if(typeof send==='function'){ + await new Promise(r=>setTimeout(r,400)); + await send(); + } + if(typeof showToast==='function') showToast('已在新对话中执行:'+command.slice(0,32)+'…',2400); + }catch(e){ + if(typeof showToast==='function') showToast('启动失败:'+(e&&e.message||e),3000); + }finally{ + setTimeout(()=>{_smLaunchInFlight=false;},1200); + } +} + +function _smBindSidebarActions(){ + const wrap=document.getElementById('smSidebarActions'); + if(!wrap||wrap.dataset.smBound==='1') return; + wrap.dataset.smBound='1'; + wrap.addEventListener('click',ev=>{ + const btn=ev.target.closest('[data-smcmd]'); + if(!btn) return; + openSelfMediaCommand(btn.dataset.smcmd); + }); +} + +async function loadSelfMediaPanel(){ + const container=document.getElementById('selfmediaContainer'); + if(!container) return; + _smBindSidebarActions(); + + container.innerHTML=` +
+
+

📡 自媒体中心

+
Hermes 自媒体自动化管理面板
+
加载中…
+
+
+
-
全部任务
+
-
运行中
+
-
自媒体
+
-
今日执行
+
+
+
⚡ 快捷操作
+
+ + + + + + +
+
+
+
+
📋 自媒体任务
+ +
+
加载中…
+
+
+
🕐 最近执行
+
加载中…
+
+
+ `; + + const dash=container.querySelector('.sm-dash'); + + async function smLoad(){ + const dot=dash.querySelector('.sm-dot'); + const txt=dash.querySelector('.sm-conn-text'); + if(dot) dot.className='sm-dot checking'; + if(txt) txt.textContent='加载中…'; + + let jobs=[], recent=[]; + try{ + const data=await api('/api/crons'); + jobs=Array.isArray(data.jobs)?data.jobs:[]; + try{ + const st=await api('/api/crons/status'); + const running=(st&&st.running)||{}; + jobs.forEach(j=>{j.running=!!running[j.id];}); + }catch(_){} + try{ + const rec=await api('/api/crons/recent'); + recent=Array.isArray(rec.completions)?rec.completions:[]; + }catch(_){} + }catch(e){ + if(dot) dot.className='sm-dot error'; + if(txt) txt.textContent='连接失败'; + dash.querySelector('#smJobs').innerHTML='
⚠️ 加载失败:'+_smEsc(e&&e.message||e)+'
'; + return; + } + + const smJobs=jobs.filter(_smIsSelfMedia); + const dayStart=new Date(); dayStart.setHours(0,0,0,0); + const todayRuns=recent.filter(r=>r.completed_at&&new Date(r.completed_at*1000)>=dayStart); + + dash.querySelector('#smStatTotal').textContent=jobs.length; + dash.querySelector('#smStatActive').textContent=jobs.filter(j=>j.running).length; + dash.querySelector('#smStatSm').textContent=smJobs.length+'/'+jobs.length; + dash.querySelector('#smStatToday').textContent=todayRuns.length; + dash.querySelector('#smCount').textContent='('+smJobs.length+'/'+jobs.length+')'; + if(dot) dot.className='sm-dot connected'; + if(txt) txt.textContent='已连接 · '+smJobs.length+' 个自媒体任务'; + + // jobs + const showAll=dash.querySelector('#smShowAll').checked; + const list=showAll?jobs:smJobs; + const jbox=dash.querySelector('#smJobs'); + if(!list.length){ + jbox.innerHTML='
'+(showAll?'暂无 cron 任务':'未识别到自媒体任务')+'
任务名含「公众号 / wechat / publish」等关键词会自动归类
'; + }else{ + jbox.innerHTML='
'; + const wrap=jbox.querySelector('.sm-jobs'); + list.forEach(job=>{ + const isSm=_smIsSelfMedia(job); + const badge=job.paused?['paused','已暂停']:job.running?['running','运行中']:['idle','待命中']; + const el=document.createElement('div'); + el.className='sm-job'+(isSm?' is-sm':''); + el.innerHTML='
'+(isSm?'📱 ':'')+_smEsc(job.name||'未命名')+ + '
🕐 '+_smEsc(job.schedule||(job.no_agent?'脚本':'手动'))+ + '📦 '+(job.no_agent?'脚本':'Agent')+''+(job.profile?'👤 '+_smEsc(job.profile)+'':'')+ + '
● '+badge[1]+'
'+ + ''; + wrap.appendChild(el); + }); + wrap.querySelectorAll('.sm-run').forEach(btn=>{ + btn.addEventListener('click',async function(){ + const id=this.dataset.id; + this.disabled=true; this.textContent='⋯'; + try{ + await api('/api/crons/run',{method:'POST',body:JSON.stringify({job_id:id})}); + this.textContent='✓'; + if(typeof showToast==='function') showToast('任务已触发',1600); + setTimeout(()=>{this.textContent='▶';this.disabled=false;},2000); + setTimeout(smLoad,3000); + }catch(e){ + this.textContent='✗'; this.disabled=false; + if(typeof showToast==='function') showToast('触发失败:'+(e&&e.message||e),3000); + } + }); + }); + } + + // recent + const rbox=dash.querySelector('#smRecent'); + if(!recent.length){ + rbox.innerHTML='
暂无执行记录
'; + }else{ + rbox.innerHTML='
'; + const wrap=rbox.querySelector('.sm-acts'); + recent.slice(0,20).forEach(r=>{ + const ok=r.status!=='failed'&&r.status!=='error'; + const el=document.createElement('div'); + el.className='sm-act'; + el.innerHTML=''+_smEsc(r.completed_at?_smAgo(new Date(r.completed_at*1000)):'进行中')+ + ''+_smEsc(r.name||r.job_name||'未知')+ + ''+(ok?'✓':'✗')+''; + wrap.appendChild(el); + }); + } + } + + const actions=dash.querySelector('#smActions'); + actions.addEventListener('click',ev=>{ + const btn=ev.target.closest('[data-cmd]'); + if(!btn) return; + const cmd=btn.dataset.cmd; + if(cmd==='__refresh__'){ smLoad(); return; } + const label=btn.textContent; + btn.disabled=true; btn.textContent='⏳ 启动中…'; + openSelfMediaCommand(cmd).finally(()=>{ + setTimeout(()=>{btn.disabled=false;btn.textContent=label;},2200); + }); + }); + dash.querySelector('#smShowAll').addEventListener('change',smLoad); + + await smLoad(); + if(window._smTimer) clearInterval(window._smTimer); + window._smTimer=setInterval(()=>{ + const el=document.getElementById('selfmediaContainer'); + if(el&&el.offsetParent!==null) smLoad(); + },30000); +} + // ── Providers panel ───────────────────────────────────────────────────────── const _providerCardEls = new Map(); // providerId → entry used by save/remove/test handlers diff --git a/static/style.css b/static/style.css index ef72a752..6f540a77 100644 --- a/static/style.css +++ b/static/style.css @@ -4941,7 +4941,7 @@ main.main > #mainWorkspaces, main.main > #mainProfiles, main.main > #mainInsights, main.main > #mainLogs{display:none;} -main.main:not(.showing-settings):not(.showing-skills):not(.showing-memory):not(.showing-tasks):not(.showing-kanban):not(.showing-workspaces):not(.showing-profiles):not(.showing-insights):not(.showing-logs):not(.showing-plugin) > #mainChat{display:flex;} +main.main:not(.showing-settings):not(.showing-skills):not(.showing-memory):not(.showing-tasks):not(.showing-kanban):not(.showing-workspaces):not(.showing-profiles):not(.showing-insights):not(.showing-logs):not(.showing-plugin):not(.showing-selfmedia) > #mainChat{display:flex;} main.main.showing-settings > #mainSettings{display:flex;overflow-y:auto;} main.main.showing-skills > #mainSkills{display:flex;} main.main.showing-memory > #mainMemory{display:flex;} @@ -4951,8 +4951,11 @@ main.main.showing-workspaces > #mainWorkspaces{display:flex;} main.main.showing-profiles > #mainProfiles{display:flex;} main.main.showing-logs > #mainLogs{display:flex;} main.main.showing-plugin > #mainPlugin{display:flex;} -main.main > #mainPlugin{display:none;} +main.main.showing-selfmedia > #mainSelfmedia{display:flex;} +main.main > #mainPlugin, +main.main > #mainSelfmedia{display:none;} #mainPlugin .main-view-body{flex:1;display:flex;flex-direction:column;overflow:hidden;} +#mainSelfmedia .main-view-body{flex:1;display:flex;flex-direction:column;overflow:hidden;} #pluginPageContainer{flex:1;display:flex;flex-direction:column;min-height:0;} #mainSettings{overflow-y:auto;}