From d8401bfa058b0322ea25ca079cbdbb97eec3c466 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Wed, 4 Mar 2026 16:16:05 +0800 Subject: [PATCH 1/9] Fix dispatch: remove hardcoded feishu channel --- .gitignore | 3 +++ dashboard/server.py | 8 +++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 13613a5..9133f10 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,9 @@ venv/ node_modules/ edict/frontend/node_modules/ +# Local env +edict/.env + # Backups *.bak* data diff --git a/dashboard/server.py b/dashboard/server.py index 57fd64b..8f5080f 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -1956,8 +1956,14 @@ def _do_dispatch(): 'lastDispatchTrigger': trigger, })) return + # NOTE: do not hardcode channel=feishu. In environments without Feishu channel, + # this causes: invalid agent params: unknown channel: feishu + # Use the task's org as deliver target when available; otherwise let OpenClaw pick default. cmd = ['openclaw', 'agent', '--agent', agent_id, '-m', msg, - '--deliver', '--channel', 'feishu', '--timeout', '300'] + '--deliver', '--timeout', '300'] + deliver_org = task.get('org') + if deliver_org: + cmd.extend(['--org', deliver_org]) max_retries = 2 err = '' for attempt in range(1, max_retries + 1): From d5f0c731715419f3bd98df54dba0d9c485f30e73 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Wed, 4 Mar 2026 16:29:52 +0800 Subject: [PATCH 2/9] Fix dispatch: remove unsupported --org and --deliver --- dashboard/server.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dashboard/server.py b/dashboard/server.py index 8f5080f..c734f33 100644 --- a/dashboard/server.py +++ b/dashboard/server.py @@ -1958,12 +1958,11 @@ def _do_dispatch(): return # NOTE: do not hardcode channel=feishu. In environments without Feishu channel, # this causes: invalid agent params: unknown channel: feishu - # Use the task's org as deliver target when available; otherwise let OpenClaw pick default. + # Also avoid --deliver unless you can provide a valid --channel/--reply-channel. + # For automated dispatch, we only need to trigger the agent turn; agents will + # update the kanban themselves via scripts. cmd = ['openclaw', 'agent', '--agent', agent_id, '-m', msg, - '--deliver', '--timeout', '300'] - deliver_org = task.get('org') - if deliver_org: - cmd.extend(['--org', deliver_org]) + '--timeout', '300'] max_retries = 2 err = '' for attempt in range(1, max_retries + 1): From 161f7994a538dcc94e0caa6f202464f771c79b91 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Thu, 5 Mar 2026 11:27:55 +0800 Subject: [PATCH 3/9] chore: ignore backups and deliverables --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 9133f10..177b413 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,5 @@ edict/.env # Backups *.bak* data +backups/ +deliverables/ From af946460b3213d6c32fad76cf12ea3ac02d39a8c Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Thu, 5 Mar 2026 20:23:34 +0800 Subject: [PATCH 4/9] feat: add taizi 20min progress watchdog --- scripts/run_loop.sh | 9 ++++ scripts/taizi_20min_ping.py | 105 ++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 scripts/taizi_20min_ping.py diff --git a/scripts/run_loop.sh b/scripts/run_loop.sh index c00c7aa..2225f82 100755 --- a/scripts/run_loop.sh +++ b/scripts/run_loop.sh @@ -56,6 +56,15 @@ while true; do python3 "$SCRIPT_DIR/sync_officials_stats.py" >> "$LOG" 2>&1 || true python3 "$SCRIPT_DIR/refresh_live_data.py" >> "$LOG" 2>&1 || true + # 太子通讯督办:确保关键任务每20分钟至少回报一次(写入看板 progress)。 + # 注意:这里只对少数旗舰任务启用,避免刷屏。 + python3 "$SCRIPT_DIR/taizi_20min_ping.py" --task JJC-20260305-002 --max-minutes 20 \ + --now "强制20分钟同步:正在督办RouteA vNext交付与验收" \ + --plan "收集各部回填🔄|催办未回填项🔄|汇总回奏皇上" >> "$LOG" 2>&1 || true + python3 "$SCRIPT_DIR/taizi_20min_ping.py" --task JJC-20260305-003 --max-minutes 20 \ + --now "强制20分钟同步:正在复盘同步失效根因并落地机制" \ + --plan "收集事实与时间线🔄|定位根因与对策🔄|落地机制并验收" >> "$LOG" 2>&1 || true + # 定期巡检:检测卡住的任务并自动重试 SCAN_COUNTER=$((SCAN_COUNTER + INTERVAL)) if (( SCAN_COUNTER >= SCAN_INTERVAL )); then diff --git a/scripts/taizi_20min_ping.py b/scripts/taizi_20min_ping.py new file mode 100644 index 0000000..3d56483 --- /dev/null +++ b/scripts/taizi_20min_ping.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""taizi_20min_ping.py + +A simple watchdog that ensures the taizi agent posts a minimal progress update +at least every N minutes for specific active tasks. + +Design goals: +- Be safe: no messaging APIs; only writes to kanban via kanban_update.py progress. +- Be conservative: only ping tasks in non-terminal states. + +Usage: + python3 scripts/taizi_20min_ping.py --task JJC-... --max-minutes 20 --now "..." --plan "...|..." + +Typical use: invoked by ops/edict run_loop.sh or cron. +""" + +from __future__ import annotations + +import argparse +import datetime as dt +import json +import os +import subprocess +from pathlib import Path + +BASE = Path(__file__).resolve().parent.parent +TASKS = BASE / "data" / "tasks_source.json" +K_UPDATE = BASE / "scripts" / "kanban_update.py" + +TERMINAL = {"Done", "Cancelled"} + + +def now_utc() -> dt.datetime: + return dt.datetime.now(dt.timezone.utc) + + +def parse_iso(s: str) -> dt.datetime | None: + try: + return dt.datetime.fromisoformat(s.replace("Z", "+00:00")) + except Exception: + return None + + +def load_tasks() -> list[dict]: + if not TASKS.exists(): + return [] + return json.loads(TASKS.read_text()) + + +def find_task(tasks: list[dict], task_id: str) -> dict | None: + for t in tasks: + if t.get("id") == task_id: + return t + return None + + +def last_progress_at(task: dict) -> dt.datetime | None: + pl = task.get("progress_log") or [] + if not pl: + return parse_iso(task.get("updatedAt") or "") + last = pl[-1] + return parse_iso(last.get("at") or "") or parse_iso(task.get("updatedAt") or "") + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--task", required=True) + ap.add_argument("--max-minutes", type=int, default=20) + ap.add_argument("--now", required=True) + ap.add_argument("--plan", required=True) + args = ap.parse_args() + + tasks = load_tasks() + task = find_task(tasks, args.task) + if not task: + return 0 + + if (task.get("state") in TERMINAL) or (task.get("org") in ("完成",)): + return 0 + + last = last_progress_at(task) + if not last: + # no timestamp: ping once + should = True + else: + gap = (now_utc() - last).total_seconds() / 60 + should = gap >= args.max_minutes + + if not should: + return 0 + + cmd = [ + "python3", + str(K_UPDATE), + "progress", + args.task, + args.now, + args.plan, + ] + subprocess.run(cmd, cwd=str(BASE), check=False) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) From c8fe8769c4b38180dba80cb76b9df67c9521b4d1 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Fri, 6 Mar 2026 01:42:41 +0800 Subject: [PATCH 5/9] prevent JJC state downgrade in sync_from_openclaw_runtime --- scripts/sync_from_openclaw_runtime.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/sync_from_openclaw_runtime.py b/scripts/sync_from_openclaw_runtime.py index 8b3de21..7d3ee6f 100644 --- a/scripts/sync_from_openclaw_runtime.py +++ b/scripts/sync_from_openclaw_runtime.py @@ -312,7 +312,23 @@ def main(): try: existing = json.loads(existing_tasks_file.read_text()) jjc_existing = [t for t in existing if str(t.get('id', '')).startswith('JJC')] - + + # ✅ 防止 JJC 任务 state 被旧快照误降级: + # 1) 默认以 progress_log 最近一次上报的 state 为准(若存在) + # 2) 但加入“反降级”硬规则:若当前 state 属于执行态(Doing/Review/Blocked),禁止降级为 Assigned/Next/Inbox。 + for t in jjc_existing: + current_state = str(t.get('state') or '').strip() + pl = t.get('progress_log') or [] + if pl: + last = pl[-1] or {} + last_state = str(last.get('state') or '').strip() + if last_state: + # 反降级:执行态不得被 progress 尾部错误固化为 Assigned/Next/Inbox + if current_state in ('Doing', 'Review', 'Blocked') and last_state in ('Assigned', 'Next', 'Inbox'): + pass + else: + t['state'] = last_state + # 去掉 tasks 里已有的 JJC(以防重复),再把旨意放到最前面 tasks = [t for t in tasks if not str(t.get('id', '')).startswith('JJC')] tasks = jjc_existing + tasks From cf33d38a8198811b6a5fd0338d99bebfe680fd29 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Fri, 6 Mar 2026 11:06:09 +0800 Subject: [PATCH 6/9] fix: persist workspace data symlink setup in install script --- install.sh | 46 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 6dc4038..40e77ae 100755 --- a/install.sh +++ b/install.sh @@ -24,6 +24,8 @@ warn() { echo -e "${YELLOW}⚠️ $1${NC}"; } error() { echo -e "${RED}❌ $1${NC}"; } info() { echo -e "${BLUE}ℹ️ $1${NC}"; } +AGENTS=(taizi zhongshu menxia shangshu hubu libu bingbu xingbu gongbu libu_hr zaochao) + # ── Step 0: 依赖检查 ────────────────────────────────────────── check_deps() { info "检查依赖..." @@ -91,8 +93,7 @@ backup_existing() { # ── Step 1: 创建 Workspace ────────────────────────────────── create_workspaces() { info "创建 Agent Workspace..." - - AGENTS=(taizi zhongshu menxia shangshu hubu libu bingbu xingbu gongbu libu_hr zaochao) + for agent in "${AGENTS[@]}"; do ws="$OC_HOME/workspace-$agent" mkdir -p "$ws/skills" @@ -120,6 +121,46 @@ AGENTS_EOF done } +# ── Step 1.5: 统一 data 软链到仓库 data 目录 ─────────────────── +link_workspace_data() { + info "为 Agent Workspace 建立 data 软链..." + + local target_data="$REPO_DIR/data" + local target_real + target_real="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$target_data")" + + local has_workspace=false + for ws in "$OC_HOME"/workspace-*; do + [ -d "$ws" ] || continue + has_workspace=true + + local ws_data="$ws/data" + local ws_name + ws_name="$(basename "$ws")" + + if [ -L "$ws_data" ]; then + local current_real + current_real="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$ws_data")" + if [ "$current_real" = "$target_real" ]; then + log "$ws_name: data 软链已正确指向 $target_data" + continue + fi + mv "$ws_data" "$ws_data.bak.$(date +%Y%m%d-%H%M%S)" + warn "$ws_name: 旧 data 软链已备份,重新建立指向 $target_data" + elif [ -e "$ws_data" ]; then + mv "$ws_data" "$ws_data.bak.$(date +%Y%m%d-%H%M%S)" + warn "$ws_name: 旧 data 目录/文件已备份,重新建立软链" + fi + + ln -s "$target_data" "$ws_data" + log "$ws_name: data -> $target_data" + done + + if ! $has_workspace; then + warn "未检测到 workspace-* 目录,跳过 data 软链建立" + fi +} + # ── Step 2: 注册 Agents ───────────────────────────────────── register_agents() { info "注册三省六部 Agents..." @@ -277,6 +318,7 @@ backup_existing create_workspaces register_agents init_data +link_workspace_data build_frontend first_sync restart_gateway From 7f454009d575ceb340ab777c767982c8fcb0b667 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Fri, 6 Mar 2026 11:09:41 +0800 Subject: [PATCH 7/9] =?UTF-8?q?fix:=20=E4=BB=85=E4=B8=BA=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E7=9B=AE=E6=A0=87=20agent=20=E5=BB=BA=E7=AB=8B=20workspace=20d?= =?UTF-8?q?ata=20=E8=BD=AF=E9=93=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- install.sh | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/install.sh b/install.sh index 40e77ae..1093fd7 100755 --- a/install.sh +++ b/install.sh @@ -130,8 +130,12 @@ link_workspace_data() { target_real="$(python3 -c 'import os,sys;print(os.path.realpath(sys.argv[1]))' "$target_data")" local has_workspace=false - for ws in "$OC_HOME"/workspace-*; do - [ -d "$ws" ] || continue + for agent in "${AGENTS[@]}"; do + local ws="$OC_HOME/workspace-$agent" + [ -d "$ws" ] || { + warn "workspace-$agent 不存在,跳过 data 软链建立" + continue + } has_workspace=true local ws_data="$ws/data" @@ -157,7 +161,7 @@ link_workspace_data() { done if ! $has_workspace; then - warn "未检测到 workspace-* 目录,跳过 data 软链建立" + warn "未检测到安装脚本目标 Workspace,跳过 data 软链建立" fi } From 013d3182ba8d5a7d05bfbdfae0bb04666af604b6 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Fri, 6 Mar 2026 11:33:20 +0800 Subject: [PATCH 8/9] =?UTF-8?q?fix:=20=E7=BB=9F=E4=B8=80=E7=9C=8B=E6=9D=BF?= =?UTF-8?q?=E6=97=B6=E9=97=B4=E8=A7=A3=E6=9E=90=E5=B9=B6=E6=8C=89=E6=9C=AC?= =?UTF-8?q?=E5=9C=B0=E6=97=B6=E5=8C=BA=E6=98=BE=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dashboard/dashboard.html | 57 +++++++++++++++++++++++++++++++--------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/dashboard/dashboard.html b/dashboard/dashboard.html index 515f2eb..f1b46e4 100644 --- a/dashboard/dashboard.html +++ b/dashboard/dashboard.html @@ -889,7 +889,7 @@ globalScanCheckedAt = String(raw.checkedAt || ''); globalScanCount = Number(raw.count || globalScanActions.length || 0); if(st){ - const at = globalScanCheckedAt.replace('T',' ').substring(11,19); + const at = fmtActivityTime(globalScanCheckedAt); const count = globalScanCount; st.textContent = count || at ? `最近巡检: ${count} 个动作${at ? ' · ' + at : ''}` : '未巡检'; } @@ -1105,7 +1105,7 @@ ${idle} 待命 ${offline?` ${offline} 离线`:''} ${unconf?` ${unconf} 未配置`:''} - 检测于 ${(data.checkedAt||'').substring(11,19)} + 检测于 ${fmtActivityTime(data.checkedAt||'')} `; } @@ -1703,16 +1703,48 @@ } } -function fmtActivityTime(ts){ - if(!ts)return ''; +function parseDateFlexible(ts){ + if(ts===null||ts===undefined||ts==='') return null; + if(ts instanceof Date) return isNaN(ts.getTime())?null:ts; + if(typeof ts==='number'){ - const d=new Date(ts); - return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`; + // 兼容秒级与毫秒级时间戳 + const ms = ts > 1e12 ? ts : ts * 1000; + const d = new Date(ms); + return isNaN(d.getTime()) ? null : d; } - if(typeof ts==='string'&&ts.length>=19){ - return ts.substring(11,19); + + if(typeof ts!=='string') return null; + const s = ts.trim(); + if(!s) return null; + + if(/^\d+$/.test(s)){ + const n = Number(s); + if(Number.isFinite(n)){ + const ms = n > 1e12 ? n : n * 1000; + const d = new Date(ms); + return isNaN(d.getTime()) ? null : d; + } } - return String(ts).substring(0,8); + + // "YYYY-MM-DD HH:mm:ss" 视为本地时间(历史数据兼容) + const localMatch = s.match(/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/); + if(localMatch){ + const [, y, m, d, h, mi, se] = localMatch; + const dt = new Date(Number(y), Number(m)-1, Number(d), Number(h), Number(mi), Number(se)); + return isNaN(dt.getTime()) ? null : dt; + } + + // ISO 8601(含 Z / +08:00)走浏览器标准解析,自动换算到本地显示 + const iso = s.includes(' ') ? s.replace(' ', 'T') : s; + const parsed = new Date(iso); + return isNaN(parsed.getTime()) ? null : parsed; +} + +function fmtActivityTime(ts){ + const d = parseDateFlexible(ts); + if(!d) return ''; + return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}:${String(d.getSeconds()).padStart(2,'0')}`; } /* ══ MODEL CONFIG ══ */ @@ -2101,7 +2133,7 @@ const kLabel = kind==='assistant'?'回复':kind==='tool'?'工具':kind==='user'?'用户':'事件'; let txt = (a.text||'').replace(/\[\[.*?\]\]/g,'').replace(/\*\*/g,'').trim(); if(txt.length>200) txt=txt.substring(0,200)+'…'; - const time = (a.at||'').substring(11,19); + const time = fmtActivityTime(a.at); return `
${kIcon} @@ -2134,7 +2166,8 @@ function timeAgo(iso){ if(!iso) return ''; try{ - const d = new Date(iso.includes('T')?iso:iso.replace(' ','T')+'Z'); + const d = parseDateFlexible(iso); + if(!d) return ''; if(isNaN(d.getTime())) return ''; const diff = Date.now() - d.getTime(); const mins = Math.floor(diff/60000); @@ -2628,7 +2661,7 @@ const r = await postJ(API + '/scheduler-scan', {thresholdSec: 180}); if(r.ok){ const count = r.count || 0; - const at = (r.checkedAt || '').replace('T',' ').substring(11,19); + const at = fmtActivityTime(r.checkedAt || ''); globalScanActions = Array.isArray(r.actions) ? r.actions : []; globalScanCheckedAt = String(r.checkedAt || ''); globalScanCount = Number(count || 0); From 20168cd3b1d9ace8613441df06755616dd9a7d08 Mon Sep 17 00:00:00 2001 From: OpenClaw Taizi Date: Fri, 6 Mar 2026 15:33:33 +0800 Subject: [PATCH 9/9] =?UTF-8?q?fix:=20=E5=AF=B9=E9=BD=90Task=E5=85=83?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E5=AD=97=E6=AE=B5=E5=B9=B6=E7=BB=9F=E4=B8=80?= =?UTF-8?q?scheduler=E8=BE=93=E5=87=BA=E9=94=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 TaskService 创建任务时传入 meta 与 Task 模型字段不一致的问题,避免运行时构造任务失败。同步将任务序列化输出的 _scheduler 统一为 scheduler,并回传 meta,降低前后端字段歧义。 --- edict/backend/app/models/task.py | 4 +++- edict/backend/app/services/task_service.py | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/edict/backend/app/models/task.py b/edict/backend/app/models/task.py index 13365ef..ff345e1 100644 --- a/edict/backend/app/models/task.py +++ b/edict/backend/app/models/task.py @@ -98,6 +98,7 @@ class Task(Base): scheduler = Column(JSONB, default=dict, comment="调度器元数据") template_id = Column(String(64), default="", comment="模板ID") template_params = Column(JSONB, default=dict, comment="模板参数") + meta = Column(JSONB, default=dict, comment="元数据") ac = Column(Text, default="", comment="验收标准") target_dept = Column(String(64), default="", comment="目标部门") @@ -136,7 +137,8 @@ def to_dict(self) -> dict: "templateParams": self.template_params or {}, "ac": self.ac, "targetDept": self.target_dept, - "_scheduler": self.scheduler or {}, + "scheduler": self.scheduler or {}, "createdAt": self.created_at.isoformat() if self.created_at else "", + "meta": self.meta or {}, "updatedAt": self.updated_at.isoformat() if self.updated_at else "", } diff --git a/edict/backend/app/services/task_service.py b/edict/backend/app/services/task_service.py index 3399b73..01c8c62 100644 --- a/edict/backend/app/services/task_service.py +++ b/edict/backend/app/services/task_service.py @@ -86,6 +86,7 @@ async def create_task( "state": initial_state.value, "priority": priority, "assignee_org": assignee_org, + "meta": meta or {}, }, )