mirror of
https://github.com/cft0808/edict.git
synced 2026-09-09 19:01:13 +08:00
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
226 lines
7.1 KiB
Python
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()
|