diff --git a/server.py b/server.py
index 904be14e..83bc5684 100644
--- a/server.py
+++ b/server.py
@@ -2472,6 +2472,40 @@ def api_broadcast_archive():
return jsonify({"ok": True})
+
+@app.route("/api/broadcast/toggle_delivery", methods=["POST"])
+def api_broadcast_toggle_delivery():
+ """切换 cron job 的消息通道(broadcast/xmpp/both)
+ payload: {"name": "job名称"} 或 {"script": "脚本名"}
+ """
+ import json as _json
+ data = request.get_json(force=True) or {}
+ jobs_path = "/home/hmo/.hermes/profiles/position-analyst/cron/jobs.json"
+ with open(jobs_path, encoding="utf-8") as f:
+ jobs = _json.load(f)
+ update = None
+ for j in jobs.get("jobs", []):
+ if not isinstance(j, dict):
+ continue
+ match = False
+ if data.get("name") and j.get("name") == data.get("name"):
+ match = True
+ elif data.get("script") and str(j.get("script","")) == data.get("script"):
+ match = True
+ if match:
+ # 循环切换
+ cur = j.get("delivery", "broadcast")
+ nxt = {"broadcast":"xmpp", "xmpp":"both", "both":"broadcast"}.get(cur, "broadcast")
+ j["delivery"] = nxt
+ update = {"name": j.get("name"), "script": j.get("script"), "delivery": nxt}
+ break
+ if update:
+ with open(jobs_path, "w", encoding="utf-8") as f:
+ _json.dump(jobs, f, ensure_ascii=False, indent=2)
+ return jsonify({"ok": True, "job": update})
+ return jsonify({"ok": False, "error": "job not found"})
+
+
if __name__ == "__main__":
port = int(os.environ.get("PORT", 8899))
print(f"🚀 MoFin Dashboard → http://0.0.0.0:{port}")
diff --git a/static/mofin_health.html b/static/mofin_health.html
index 84923ce8..dbbebd86 100644
--- a/static/mofin_health.html
+++ b/static/mofin_health.html
@@ -390,7 +390,7 @@ function renderPipelineTable(pipelines) {
const layerColor = {collect:'#3fb950', process:'#d29922', use:'#58a6ff', monitor:'#bc8cff'}[layer];
html += `
`;
html += `${title} ${desc} · ${group.length}任务 ✅${ok} ❌${err}
`;
- html += '| 名称 | 来源 | 脚本/LLM | 类型 | 调度 | 状态 | 最后运行 | 最后十次 |
';
+ html += '| 名称 | 来源 | 脚本/LLM | 类型 | 调度 | 消息通道 | 状态 | 最后运行 | 最后十次 |
';
group.forEach((p) => {
const tagCls = p.status==='ok'?'ok':p.status==='error'?'error':'warn';
const statusDisplay = p.last_run ? p.status : '待首次运行';
@@ -408,6 +408,9 @@ function renderPipelineTable(pipelines) {
html += `${p.script||'LLM'} | `;
html += `${p.type||'cron'} | `;
html += `${p.schedule||'-'} | `;
+ const delColor = p.delivery==='xmpp'?'#3fb950':p.delivery==='both'?'#d29922':'#8b949e';
+ const delLabel = p.delivery==='xmpp'?'📣 xmpp':p.delivery==='both'?'🔄 both':'📡 broadcast';
+ html += `${delLabel} | `;
html += `${statusDisplay} | `;
html += `${p.last_run||'-'} | `;
html += ` | `;
@@ -581,5 +584,24 @@ function viewReportDetail(reportId) {
list.innerHTML = '加载失败: ' + e.message + '
';
});
}
+
+function cycDelivery(jobName) {
+ var uname = decodeURIComponent(jobName);
+ var row = document.querySelector('[data-name="'+uname.toLowerCase()+'"]');
+ if (!row) { alert('job not found'); return; }
+ var cur = row.querySelector('.pipe-row span') ? row.querySelector('.pipe-row span').textContent : '';
+ // 找到该行的delivery span
+ var spans = row.querySelectorAll('td:nth-child(6) span');
+ var curDel = spans[0] ? spans[0].textContent.replace(/[^ -]/g,'').trim() : '';
+ // 简化:点击后调API toggle
+ fetch('/api/broadcast/toggle_delivery', {
+ method:'POST',
+ headers:{'Content-Type':'application/json'},
+ body: JSON.stringify({name: uname})
+ }).then(r=>r.json()).then(d=>{
+ location.reload();
+ }).catch(e=>alert('切换失败:'+e.message));
+}
+