mirror of
https://github.com/cft0808/edict.git
synced 2026-09-03 07:17:16 +08:00
feat: multi-workspace task data auto-detection + time parsing improvements
- Auto-detect task data dir from ~/.openclaw/workspace-*/data (#117) - Score and select best task source (non-demo tasks preferred) - Update healthz and live-status to use detected data dir - Add robust parseDateFlexible() for timestamp handling (#67) - Add UTF-8 encoding to file_lock reads for Windows compat (#96) - Use absolute path in install.sh hint (#107) Closes #117, Closes #107
This commit is contained in:
@@ -1714,16 +1714,40 @@ function renderLiveActivity(data){
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
}
|
||||
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 ══ */
|
||||
@@ -2149,8 +2173,8 @@ function extractAgent(t){
|
||||
function timeAgo(iso){
|
||||
if(!iso) return '';
|
||||
try{
|
||||
const d = new Date(iso.includes('T')?iso:iso.replace(' ','T')+'Z');
|
||||
if(isNaN(d.getTime())) return '';
|
||||
const d = parseDateFlexible(iso);
|
||||
if(!d) return '';
|
||||
const diff = Date.now() - d.getTime();
|
||||
const mins = Math.floor(diff/60000);
|
||||
if(mins<0) return '刚刚';
|
||||
|
||||
@@ -50,6 +50,7 @@ BASE = pathlib.Path(__file__).parent
|
||||
DIST = BASE / 'dist' # React 构建产物 (npm run build)
|
||||
DATA = BASE.parent / "data"
|
||||
SCRIPTS = BASE.parent / 'scripts'
|
||||
_ACTIVE_TASK_DATA_DIR = None
|
||||
|
||||
# 静态资源 MIME 类型
|
||||
_MIME_TYPES = {
|
||||
@@ -83,16 +84,67 @@ def cors_headers(h):
|
||||
h.send_header('Access-Control-Allow-Headers', 'Content-Type')
|
||||
|
||||
|
||||
def _iter_task_data_dirs():
|
||||
"""返回可用的任务数据目录候选(优先 workspace,其次本地 data)。"""
|
||||
dirs = [DATA]
|
||||
for p in sorted(OCLAW_HOME.glob('workspace-*/data')):
|
||||
if p.is_dir():
|
||||
dirs.append(p)
|
||||
return dirs
|
||||
|
||||
|
||||
def _task_source_score(task_file: pathlib.Path):
|
||||
"""给任务源打分:优先非 demo 任务,其次任务数,再按文件更新时间。"""
|
||||
try:
|
||||
tasks = atomic_json_read(task_file, [])
|
||||
except Exception:
|
||||
tasks = []
|
||||
if not isinstance(tasks, list):
|
||||
tasks = []
|
||||
non_demo = sum(1 for t in tasks if str((t or {}).get('id', '')) and not str((t or {}).get('id', '')).startswith('JJC-DEMO'))
|
||||
try:
|
||||
mtime = task_file.stat().st_mtime
|
||||
except Exception:
|
||||
mtime = 0
|
||||
return (1 if non_demo > 0 else 0, non_demo, len(tasks), mtime)
|
||||
|
||||
|
||||
def get_task_data_dir():
|
||||
"""自动选择当前任务数据目录,并缓存结果以保持一次服务期内稳定。"""
|
||||
global _ACTIVE_TASK_DATA_DIR
|
||||
if _ACTIVE_TASK_DATA_DIR and _ACTIVE_TASK_DATA_DIR.is_dir():
|
||||
return _ACTIVE_TASK_DATA_DIR
|
||||
best_dir = DATA
|
||||
best_score = (-1, -1, -1, -1)
|
||||
for d in _iter_task_data_dirs():
|
||||
tf = d / 'tasks_source.json'
|
||||
if not tf.exists():
|
||||
continue
|
||||
score = _task_source_score(tf)
|
||||
if score > best_score:
|
||||
best_score = score
|
||||
best_dir = d
|
||||
_ACTIVE_TASK_DATA_DIR = best_dir
|
||||
log.info(f'任务数据源: {_ACTIVE_TASK_DATA_DIR}')
|
||||
return _ACTIVE_TASK_DATA_DIR
|
||||
|
||||
|
||||
def load_tasks():
|
||||
return atomic_json_read(DATA / 'tasks_source.json', [])
|
||||
task_data_dir = get_task_data_dir()
|
||||
return atomic_json_read(task_data_dir / 'tasks_source.json', [])
|
||||
|
||||
|
||||
def save_tasks(tasks):
|
||||
atomic_json_write(DATA / 'tasks_source.json', tasks)
|
||||
task_data_dir = get_task_data_dir()
|
||||
atomic_json_write(task_data_dir / 'tasks_source.json', tasks)
|
||||
# Trigger refresh (异步,不阻塞,避免僵尸进程)
|
||||
script = task_data_dir.parent / 'scripts' / 'refresh_live_data.py'
|
||||
if not script.exists():
|
||||
script = SCRIPTS / 'refresh_live_data.py'
|
||||
|
||||
def _refresh():
|
||||
try:
|
||||
subprocess.run(['python3', str(SCRIPTS / 'refresh_live_data.py')], timeout=30)
|
||||
subprocess.run(['python3', str(script)], timeout=30)
|
||||
except Exception as e:
|
||||
log.warning(f'refresh_live_data.py 触发失败: {e}')
|
||||
threading.Thread(target=_refresh, daemon=True).start()
|
||||
@@ -2168,12 +2220,14 @@ class Handler(BaseHTTPRequestHandler):
|
||||
if p in ('', '/dashboard', '/dashboard.html'):
|
||||
self.send_file(DIST / 'index.html')
|
||||
elif p == '/healthz':
|
||||
checks = {'dataDir': DATA.is_dir(), 'tasksReadable': (DATA / 'tasks_source.json').exists()}
|
||||
checks['dataWritable'] = os.access(str(DATA), os.W_OK)
|
||||
task_data_dir = get_task_data_dir()
|
||||
checks = {'dataDir': task_data_dir.is_dir(), 'tasksReadable': (task_data_dir / 'tasks_source.json').exists()}
|
||||
checks['dataWritable'] = os.access(str(task_data_dir), os.W_OK)
|
||||
all_ok = all(checks.values())
|
||||
self.send_json({'status': 'ok' if all_ok else 'degraded', 'ts': now_iso(), 'checks': checks})
|
||||
elif p == '/api/live-status':
|
||||
self.send_json(read_json(DATA / 'live_status.json'))
|
||||
task_data_dir = get_task_data_dir()
|
||||
self.send_json(read_json(task_data_dir / 'live_status.json'))
|
||||
elif p == '/api/agent-config':
|
||||
self.send_json(read_json(DATA / 'agent_config.json'))
|
||||
elif p == '/api/model-change-log':
|
||||
|
||||
@@ -431,7 +431,7 @@ echo " 1. 配置 API Key(如尚未配置):"
|
||||
echo " openclaw agents add taizi # 按提示输入 Anthropic API Key"
|
||||
echo " ./install.sh # 重新运行以同步到所有 Agent"
|
||||
echo " 2. 启动数据刷新循环: bash scripts/run_loop.sh &"
|
||||
echo " 3. 启动看板服务器: python3 dashboard/server.py"
|
||||
echo " 3. 启动看板服务器: python3 \"\$REPO_DIR/dashboard/server.py\""
|
||||
echo " 4. 打开看板: http://127.0.0.1:7891"
|
||||
echo ""
|
||||
warn "首次安装必须配置 API Key,否则 Agent 会报错"
|
||||
|
||||
@@ -68,7 +68,7 @@ def atomic_json_read(path: pathlib.Path, default: Any = None) -> Any:
|
||||
try:
|
||||
_lock_shared(fd)
|
||||
try:
|
||||
return json.loads(path.read_text()) if path.exists() else default
|
||||
return json.loads(path.read_text(encoding='utf-8')) if path.exists() else default
|
||||
except Exception:
|
||||
return default
|
||||
finally:
|
||||
@@ -93,7 +93,7 @@ def atomic_json_update(
|
||||
_lock_exclusive(fd)
|
||||
# Read
|
||||
try:
|
||||
data = json.loads(path.read_text()) if path.exists() else default
|
||||
data = json.loads(path.read_text(encoding='utf-8')) if path.exists() else default
|
||||
except Exception:
|
||||
data = default
|
||||
# Modify
|
||||
|
||||
Reference in New Issue
Block a user