feat(selfmedia): add 自媒体中心 native panel to sidebar
Browser smoke / changes (push) Canceled after 0s
Browser smoke / browser-smoke (push) Canceled after 0s
Conversation lifecycle (informational) / live-to-final (chromium, reconnect-scene-redraw, tests/browser_reconnect_scene_redraw.py) (push) Canceled after 0s
Conversation lifecycle (informational) / live-to-final (historical-transcript-hydration, tests/browser_historical_transcript_hydration.py) (push) Canceled after 0s
Conversation lifecycle (informational) / live-to-final (normal, normal, tests/browser_conversation_lifecycle.py) (push) Canceled after 0s
Conversation lifecycle (informational) / live-to-final (terminal-error, terminal-error, tests/browser_conversation_lifecycle.py) (push) Canceled after 0s
Tests / changes (push) Canceled after 0s
Tests / lint (push) Canceled after 0s
Tests / test (3.11, 0) (push) Canceled after 0s
Tests / test (3.11, 1) (push) Canceled after 0s
Tests / test (3.11, 2) (push) Canceled after 0s
Tests / test (3.11, 3) (push) Canceled after 0s
Tests / test (3.11, 4) (push) Canceled after 0s
Tests / test (3.12, 0) (push) Canceled after 0s
Tests / test (3.12, 1) (push) Canceled after 0s
Tests / test (3.12, 2) (push) Canceled after 0s
Tests / test (3.12, 3) (push) Canceled after 0s
Tests / test (3.12, 4) (push) Canceled after 0s
Tests / test (3.13, 0) (push) Canceled after 0s
Tests / test (3.13, 1) (push) Canceled after 0s
Tests / test (3.13, 2) (push) Canceled after 0s
Tests / test (3.13, 3) (push) Canceled after 0s
Tests / test (3.13, 4) (push) Canceled after 0s

- New 'selfmedia' panel registered in MAIN_VIEW_PANELS with sidebar
  rail/nav buttons, main view container and mobile sidebar panel
- Native in-origin rendering (no sandboxed iframe) so api() calls
  carry session cookie + CSRF and actually load cron data
- Dashboard: stats cards, self-media cron job detection by keywords,
  one-click job trigger, recent activity feed, 30s auto-refresh
- Quick actions open a brand-new conversation and send the command
  there (发布公众号/选题/写文章/多平台发布/热门排行)
This commit is contained in:
personal82555 committed 2026-10-17 21:56:24 +08:00
1 parent c296673ebf
commit 5e801d74d5
3 files changed
+324 -3

No files matched your search

+25
View File
@@ -160,6 +160,7 @@
<button class="rail-btn nav-tab has-tooltip" data-panel="profiles" onclick="switchPanel('profiles',{fromRailClick:true})" data-tooltip="Agent profiles" data-i18n-title="tab_profiles" aria-label="Agent profiles"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></button>
<button class="rail-btn nav-tab has-tooltip" data-panel="todos" onclick="switchPanel('todos',{fromRailClick:true})" data-tooltip="Current task list" data-i18n-title="tab_todos" aria-label="Todos"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg></button>
<button class="rail-btn nav-tab has-tooltip" data-panel="insights" onclick="switchPanel('insights',{fromRailClick:true})" data-tooltip="Insights" data-i18n-title="tab_insights" aria-label="Insights"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg></button>
<button class="rail-btn nav-tab has-tooltip" data-panel="selfmedia" onclick="switchPanel('selfmedia',{fromRailClick:true})" data-tooltip="自媒体中心" data-i18n-title="tab_selfmedia" aria-label="自媒体中心"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/><path d="M7 8h2"/><path d="M7 12h2"/></svg></button>
<button class="rail-btn nav-tab dashboard-link has-tooltip" id="dashboardRailBtn" data-dashboard-link style="display:none" onclick="openHermesDashboard(event)" data-tooltip="Hermes Dashboard" data-i18n-title="tab_dashboard" aria-label="Hermes Dashboard"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg><span class="dashboard-external-badge" aria-hidden="true"></span></button>
<button class="rail-btn nav-tab has-tooltip" data-panel="logs" onclick="switchPanel('logs',{fromRailClick:true})" data-tooltip="Logs" data-i18n-title="tab_logs" aria-label="Logs"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h8"/><path d="M8 9h2"/></svg></button>
<div class="rail-spacer"></div>
@@ -179,6 +180,7 @@
<button class="nav-tab has-tooltip has-tooltip--bottom" data-panel="profiles" data-label="Profiles" onclick="switchPanel('profiles',{fromRailClick:true})" data-tooltip="Agent profiles" data-i18n-title="tab_profiles"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></button>
<button class="nav-tab has-tooltip has-tooltip--bottom" data-panel="todos" data-label="Todos" onclick="switchPanel('todos',{fromRailClick:true})" data-tooltip="Current task list" data-i18n-title="tab_todos"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="5" width="6" height="6" rx="1"/><path d="m3 17 2 2 4-4"/><path d="M13 6h8"/><path d="M13 12h8"/><path d="M13 18h8"/></svg></button>
<button class="nav-tab has-tooltip has-tooltip--bottom" data-panel="insights" data-label="Insights" onclick="switchPanel('insights',{fromRailClick:true})" data-tooltip="Insights" data-i18n-title="tab_insights"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg></button>
<button class="nav-tab has-tooltip has-tooltip--bottom" data-panel="selfmedia" data-label="自媒体中心" onclick="switchPanel('selfmedia',{fromRailClick:true})" data-tooltip="自媒体中心" data-i18n-title="tab_selfmedia"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 6v6l4 2"/><path d="M7 8h2"/><path d="M7 12h2"/></svg></button>
<button class="nav-tab dashboard-link has-tooltip has-tooltip--bottom" id="dashboardMobileBtn" data-dashboard-link data-label="Dashboard" style="display:none" onclick="openHermesDashboard(event)" data-tooltip="Hermes Dashboard" data-i18n-title="tab_dashboard"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/><path d="M9 21V9"/></svg><span class="dashboard-external-badge" aria-hidden="true"></span></button>
<button class="nav-tab has-tooltip has-tooltip--bottom" data-panel="logs" data-label="Logs" onclick="switchPanel('logs',{fromRailClick:true})" data-tooltip="Logs" data-i18n-title="tab_logs"><svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/><path d="M8 13h8"/><path d="M8 17h8"/><path d="M8 9h2"/></svg></button>
<!-- Settings button mirrored here for mobile (rail is desktop-only via @media >=768px). Keep in sync with rail entry. -->
@@ -340,6 +342,20 @@
<button type="button" class="logs-copy" id="logsCopyAll" onclick="copyLogsAll()" data-i18n="logs_copy_all">Copy all</button>
</div>
</div>
<!-- Self-Media Center sidebar panel -->
<div class="panel-view" id="panelSelfmedia">
<div class="panel-head">
<span>自媒体中心</span>
</div>
<div class="panel-head-sub">快捷操作</div>
<div style="flex:1;overflow-y:auto;padding:8px;display:flex;flex-direction:column;gap:4px" id="smSidebarActions">
<button class="side-menu-item" data-smcmd="使用 wechat-oa-publishing 技能发布微信公众号文章">📱 发布公众号</button>
<button class="side-menu-item" data-smcmd="加载 aws-wechat-article-topics 技能生成今日选题">💡 生成今日选题</button>
<button class="side-menu-item" data-smcmd="使用 wechat-article-writing 模板写一篇公众号文章">✍️ 写一篇文章</button>
<button class="side-menu-item" data-smcmd="使用 multi-platform-publishing 技能进行多平台发布">🌐 多平台发布</button>
<button class="side-menu-item" data-smcmd="执行每日热门软件排行任务(daily-software-articles-from-ranking)">📊 热门排行</button>
</div>
</div>
<!-- Settings panel (menu list; actual panes render in .main) -->
<div class="panel-view" id="panelSettings">
<div class="panel-head">
@@ -930,6 +946,15 @@
<div id="pluginPageContainer"></div>
</div>
</div>
<!-- Self-Media Center main view -->
<div id="mainSelfmedia" class="main-view">
<div class="main-view-header">
<div class="main-view-title">自媒体中心</div>
</div>
<div class="main-view-body">
<div id="selfmediaContainer" style="flex:1;overflow-y:auto;padding:16px"></div>
</div>
</div>
<div id="mainSettings" class="main-view">
<div class="settings-main">
<div class="settings-pane active" id="settingsPaneConversation">
+294 -1
View File
@@ -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=`
<div class="sm-dash">
<div class="sm-head">
<h1>📡 自媒体中心</h1>
<div class="sm-sub">Hermes 自媒体自动化管理面板</div>
<div class="sm-conn"><span class="sm-dot checking"></span><span class="sm-conn-text">加载中…</span></div>
</div>
<div class="sm-stats">
<div class="sm-stat"><div class="sm-stat-v" id="smStatTotal">-</div><div class="sm-stat-l">全部任务</div></div>
<div class="sm-stat"><div class="sm-stat-v" id="smStatActive">-</div><div class="sm-stat-l">运行中</div></div>
<div class="sm-stat"><div class="sm-stat-v" id="smStatSm">-</div><div class="sm-stat-l">自媒体</div></div>
<div class="sm-stat"><div class="sm-stat-v" id="smStatToday">-</div><div class="sm-stat-l">今日执行</div></div>
</div>
<div class="sm-sec">
<div class="sm-sec-title">⚡ 快捷操作</div>
<div class="sm-actions" id="smActions">
<button class="sm-btn sm-btn-primary" data-cmd="使用 wechat-oa-publishing 技能发布微信公众号文章">📱 发布公众号</button>
<button class="sm-btn" data-cmd="加载 aws-wechat-article-topics 技能生成今日选题">💡 生成今日选题</button>
<button class="sm-btn" data-cmd="使用 wechat-article-writing 模板写一篇公众号文章">✍️ 写一篇文章</button>
<button class="sm-btn" data-cmd="使用 multi-platform-publishing 技能进行多平台发布">🌐 多平台发布</button>
<button class="sm-btn" data-cmd="执行每日热门软件排行任务(daily-software-articles-from-ranking)">📊 热门排行</button>
<button class="sm-btn" data-cmd="__refresh__">🔄 刷新仪表盘</button>
</div>
</div>
<div class="sm-sec">
<div class="sm-sec-head">
<div class="sm-sec-title">📋 自媒体任务 <span class="sm-count" id="smCount"></span></div>
<label class="sm-toggle"><input type="checkbox" id="smShowAll"> 显示全部</label>
</div>
<div id="smJobs"><div class="sm-loading"><div class="sm-spin"></div>加载中…</div></div>
</div>
<div class="sm-sec">
<div class="sm-sec-head"><div class="sm-sec-title">🕐 最近执行</div></div>
<div id="smRecent"><div class="sm-loading"><div class="sm-spin"></div>加载中…</div></div>
</div>
</div>
<style>
.sm-dash{color:var(--text);font-size:14px;line-height:1.5}
.sm-dash .sm-head{margin-bottom:22px}
.sm-dash .sm-head h1{font-size:20px;font-weight:600;margin:0}
.sm-dash .sm-sub{font-size:12px;color:var(--muted);margin-top:3px}
.sm-dash .sm-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:10px;margin-bottom:22px}
.sm-dash .sm-stat{background:var(--surface);border:1px solid var(--border);border-radius:9px;padding:14px}
.sm-dash .sm-stat-v{font-size:26px;font-weight:700;color:var(--accent);line-height:1.2}
.sm-dash .sm-stat-l{font-size:11px;color:var(--muted);margin-top:4px;letter-spacing:.5px}
.sm-dash .sm-sec{margin-bottom:22px}
.sm-dash .sm-sec-head{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:10px}
.sm-dash .sm-sec-title{font-size:14px;font-weight:600}
.sm-dash .sm-count{color:var(--muted);font-weight:400;font-size:12px;margin-left:6px}
.sm-dash .sm-actions{display:flex;flex-wrap:wrap;gap:8px;margin-top:10px}
.sm-dash .sm-btn{display:inline-flex;align-items:center;gap:6px;padding:8px 14px;border-radius:8px;font-size:12px;font-weight:500;border:1px solid var(--border);background:var(--surface);color:var(--text);cursor:pointer;transition:border-color .15s,color .15s}
.sm-dash .sm-btn:hover{border-color:var(--accent);color:var(--accent)}
.sm-dash .sm-btn-primary{background:var(--accent);border-color:var(--accent);color:#0D0D1A}
.sm-dash .sm-btn:disabled{opacity:.55;cursor:not-allowed}
.sm-dash .sm-jobs{display:flex;flex-direction:column;gap:6px}
.sm-dash .sm-job{background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px 14px;display:flex;align-items:center;justify-content:space-between;gap:10px}
.sm-dash .sm-job.is-sm{border-left:3px solid var(--accent)}
.sm-dash .sm-job-info{flex:1;min-width:0}
.sm-dash .sm-job-name{font-size:13px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sm-dash .sm-job-meta{font-size:10px;color:var(--muted);margin-top:3px;display:flex;gap:10px;flex-wrap:wrap}
.sm-dash .sm-badge{display:inline-flex;align-items:center;gap:3px;padding:2px 8px;border-radius:999px;font-size:10px;font-weight:500}
.sm-dash .sm-badge.idle{background:rgba(134,192,139,.13);color:var(--success)}
.sm-dash .sm-badge.paused{background:rgba(224,177,93,.13);color:var(--warning)}
.sm-dash .sm-badge.running{background:rgba(198,172,143,.13);color:var(--accent)}
.sm-dash .sm-run{padding:3px 9px;font-size:11px;border-radius:6px;border:1px solid var(--border);background:var(--surface);color:var(--text);cursor:pointer}
.sm-dash .sm-run:hover{border-color:var(--accent);color:var(--accent)}
.sm-dash .sm-empty{text-align:center;padding:30px 16px;color:var(--muted);font-size:13px}
.sm-dash .sm-loading{text-align:center;padding:30px;color:var(--muted);font-size:12px}
.sm-dash .sm-spin{display:inline-block;width:20px;height:20px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:smspin .8s linear infinite;margin-bottom:10px}
@keyframes smspin{to{transform:rotate(360deg)}}
.sm-dash .sm-err{background:rgba(248,113,113,.08);border:1px solid rgba(248,113,113,.25);border-radius:8px;padding:10px 14px;color:var(--error);font-size:12px}
.sm-dash .sm-acts{display:flex;flex-direction:column;gap:4px}
.sm-dash .sm-act{background:var(--surface);border:1px solid var(--border);border-radius:6px;padding:8px 12px;font-size:11px;color:var(--muted);display:flex;align-items:center;gap:8px}
.sm-dash .sm-act-time{flex-shrink:0;min-width:46px}
.sm-dash .sm-act-name{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sm-dash .sm-conn{display:flex;align-items:center;gap:6px;margin-top:7px;font-size:10px;color:var(--muted)}
.sm-dash .sm-dot{width:7px;height:7px;border-radius:50%;flex-shrink:0}
.sm-dash .sm-dot.checking{background:var(--warning)}
.sm-dash .sm-dot.connected{background:var(--success)}
.sm-dash .sm-dot.error{background:var(--error)}
.sm-dash .sm-toggle{font-size:11px;color:var(--muted);display:inline-flex;align-items:center;gap:4px;cursor:pointer}
.sm-dash .sm-toggle input{accent-color:var(--accent)}
</style>`;
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='<div class="sm-err">⚠️ 加载失败:'+_smEsc(e&&e.message||e)+'</div>';
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='<div class="sm-empty">'+(showAll?'暂无 cron 任务':'未识别到自媒体任务')+'<br><span style="font-size:11px">任务名含「公众号 / wechat / publish」等关键词会自动归类</span></div>';
}else{
jbox.innerHTML='<div class="sm-jobs"></div>';
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='<div class="sm-job-info"><div class="sm-job-name">'+(isSm?'📱 ':'')+_smEsc(job.name||'未命名')+
'</div><div class="sm-job-meta"><span>🕐 '+_smEsc(job.schedule||(job.no_agent?'脚本':'手动'))+
'</span><span>📦 '+(job.no_agent?'脚本':'Agent')+'</span>'+(job.profile?'<span>👤 '+_smEsc(job.profile)+'</span>':'')+
'</div></div><div><span class="sm-badge '+badge[0]+'">● '+badge[1]+'</span></div>'+
'<button class="sm-run" data-id="'+_smEsc(job.id)+'" title="立即执行">▶</button>';
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='<div class="sm-empty" style="padding:16px;font-size:12px">暂无执行记录</div>';
}else{
rbox.innerHTML='<div class="sm-acts"></div>';
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='<span class="sm-act-time">'+_smEsc(r.completed_at?_smAgo(new Date(r.completed_at*1000)):'进行中')+
'</span><span class="sm-act-name">'+_smEsc(r.name||r.job_name||'未知')+
'</span><span style="color:'+(ok?'var(--success)':'var(--error)')+'">'+(ok?'✓':'✗')+'</span>';
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
+5 -2
View File
@@ -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;}