Files
edict/scripts/sync_from_openclaw_runtime.py
cft0808 5b46f67603 🏛️ init: 三省六部 OpenClaw Multi-Agent Orchestration System
Features:
- 9 specialized agents (中书省·门下省·尚书省 + 六部)
- Real-time dashboard with 6 tabs (Overview/Kanban/History/Timeline/Models/Skills)
- Model configuration with live-apply via local API server
- One-click install script
- Data sync pipeline (15s refresh loop)
- Full audit trail via flow_log
2026-02-23 22:34:55 +08:00

226 lines
7.1 KiB
Python

#!/usr/bin/env python3
import json
import pathlib
import time
import datetime
import traceback
BASE = pathlib.Path(__file__).parent.parent
DATA = BASE / 'data'
SYNC_STATUS = DATA / 'sync_status.json'
SESSIONS_ROOT = pathlib.Path.home() / '.openclaw' / 'agents'
def write_status(**kwargs):
SYNC_STATUS.write_text(json.dumps(kwargs, ensure_ascii=False, indent=2))
def ms_to_str(ts_ms):
if not ts_ms:
return '-'
try:
return datetime.datetime.fromtimestamp(ts_ms / 1000).strftime('%Y-%m-%d %H:%M:%S')
except Exception:
return '-'
def state_from_session(age_ms, aborted):
if aborted:
return 'Blocked'
if age_ms <= 2 * 60 * 1000:
return 'Doing'
if age_ms <= 60 * 60 * 1000:
return 'Review'
return 'Next'
def detect_official(agent_id):
mapping = {
'main': ('工部尚书', '工部'),
'shangshu': ('尚书令', '尚书省'),
'zhongshu': ('中书令', '中书省'),
'menxia': ('侍中', '门下省'),
'hubu': ('户部尚书', '户部'),
'libu': ('礼部尚书', '礼部'),
'bingbu': ('兵部尚书', '兵部'),
'xingbu': ('刑部尚书', '刑部'),
'gongbu': ('工部尚书', '工部'),
}
return mapping.get(agent_id, ('尚书令', '尚书省'))
def load_activity(session_file, limit=12):
p = pathlib.Path(session_file or '')
if not p.exists():
return []
rows = []
try:
lines = p.read_text(errors='ignore').splitlines()
except Exception:
return []
for ln in reversed(lines):
try:
item = json.loads(ln)
except Exception:
continue
msg = item.get('message') or {}
role = msg.get('role')
ts = item.get('timestamp') or ''
if role == 'toolResult':
tool = msg.get('toolName', '-')
details = msg.get('details') or {}
code = details.get('exitCode')
rows.append({'at': ts, 'kind': 'tool', 'text': f"{tool} completed (code={code})"})
elif role == 'assistant':
text = ''
for c in msg.get('content', []):
if c.get('type') == 'text' and c.get('text'):
text = c.get('text').strip().replace('\n', ' ')
break
if text:
rows.append({'at': ts, 'kind': 'assistant', 'text': text[:120]})
if len(rows) >= limit:
break
rows.reverse()
return rows
def build_task(agent_id, session_key, row, now_ms):
session_id = row.get('sessionId') or session_key
updated_at = row.get('updatedAt') or 0
age_ms = max(0, now_ms - updated_at) if updated_at else 99 * 24 * 3600 * 1000
aborted = bool(row.get('abortedLastRun'))
state = state_from_session(age_ms, aborted)
official, org = detect_official(agent_id)
channel = row.get('lastChannel') or (row.get('origin') or {}).get('channel') or '-'
chat_type = row.get('chatType') or (row.get('origin') or {}).get('chatType') or '-'
model = row.get('model') or '-'
title_label = (row.get('origin') or {}).get('label') or session_key
title = f"{title_label} 会话"
session_file = row.get('sessionFile', '')
return {
'id': f"OC-{agent_id}-{str(session_id)[:8]}",
'title': title,
'official': official,
'org': org,
'state': state,
'now': f"{channel}/{chat_type} · 模型 {model}",
'eta': ms_to_str(updated_at),
'block': '上次运行中断' if aborted else '',
'output': session_file,
'flow': {
'draft': f"agent={agent_id}",
'review': f"updatedAt={ms_to_str(updated_at)}",
'dispatch': f"sessionKey={session_key}",
},
'ac': '来自 OpenClaw runtime sessions 的实时映射',
'activity': load_activity(session_file, limit=10),
'sourceMeta': {
'agentId': agent_id,
'sessionKey': session_key,
'sessionId': session_id,
'updatedAt': updated_at,
'ageMs': age_ms,
'systemSent': bool(row.get('systemSent')),
'abortedLastRun': aborted,
'inputTokens': row.get('inputTokens'),
'outputTokens': row.get('outputTokens'),
'totalTokens': row.get('totalTokens'),
}
}
def main():
start = time.time()
now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
now_ms = int(time.time() * 1000)
try:
tasks = []
scan_files = 0
if SESSIONS_ROOT.exists():
for agent_dir in sorted(SESSIONS_ROOT.iterdir()):
if not agent_dir.is_dir():
continue
agent_id = agent_dir.name
sessions_file = agent_dir / 'sessions' / 'sessions.json'
if not sessions_file.exists():
continue
scan_files += 1
try:
raw = json.loads(sessions_file.read_text())
except Exception:
continue
if not isinstance(raw, dict):
continue
for session_key, row in raw.items():
if not isinstance(row, dict):
continue
tasks.append(build_task(agent_id, session_key, row, now_ms))
# merge mission control tasks (最小接入)
mc_tasks_file = DATA / 'mission_control_tasks.json'
if mc_tasks_file.exists():
try:
mc_tasks = json.loads(mc_tasks_file.read_text())
if isinstance(mc_tasks, list):
tasks.extend(mc_tasks)
except Exception:
pass
# merge manual parallel tasks (用于军机处并行看板展示)
manual_tasks_file = DATA / 'manual_parallel_tasks.json'
if manual_tasks_file.exists():
try:
manual_tasks = json.loads(manual_tasks_file.read_text())
if isinstance(manual_tasks, list):
tasks.extend(manual_tasks)
except Exception:
pass
tasks.sort(key=lambda x: x.get('sourceMeta', {}).get('updatedAt', 0), reverse=True)
(DATA / 'tasks_source.json').write_text(json.dumps(tasks, ensure_ascii=False, indent=2))
duration_ms = int((time.time() - start) * 1000)
write_status(
ok=True,
lastSyncAt=now,
durationMs=duration_ms,
source='openclaw_runtime_sessions',
recordCount=len(tasks),
scannedSessionFiles=scan_files,
missingFields={},
error=None,
)
print(f'synced {len(tasks)} tasks from openclaw runtime in {duration_ms}ms')
except Exception as e:
duration_ms = int((time.time() - start) * 1000)
write_status(
ok=False,
lastSyncAt=now,
durationMs=duration_ms,
source='openclaw_runtime_sessions',
recordCount=0,
missingFields={},
error=f'{type(e).__name__}: {e}',
traceback=traceback.format_exc(limit=3),
)
raise
if __name__ == '__main__':
main()