Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions dashboard/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,13 @@ def handle_create_task(title, org='中书省', official='中书令', priority='n
return {'ok': True, 'taskId': task_id, 'message': f'旨意 {task_id} 已下达,正在派发给太子'}


def _todo_progress(task):
todos = task.get('todos') or []
total = len(todos)
completed = sum(1 for td in todos if td.get('status') == 'completed')
return completed, total


def handle_review_action(task_id, action, comment=''):
"""门下省御批:准奏/封驳。"""
tasks = load_tasks()
Expand All @@ -706,6 +713,9 @@ def handle_review_action(task_id, action, comment=''):
remark = f'✅ 准奏:{comment or "门下省审议通过"}'
to_dept = '尚书省'
else: # Review
completed, total = _todo_progress(task)
if total > 0 and completed < total:
return {'ok': False, 'error': f'子任务尚未全部完成({completed}/{total}),不能直接准奏完结'}
task['state'] = 'Done'
task['now'] = '御批通过,任务完成'
remark = f'✅ 御批准奏:{comment or "审查通过"}'
Expand Down
2 changes: 1 addition & 1 deletion scripts/file_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def atomic_json_update(
dir=str(path.parent), suffix='.tmp', prefix=path.stem + '_'
)
try:
with os.fdopen(tmp_fd, 'w') as f:
with os.fdopen(tmp_fd, 'w', encoding='utf-8') as f:
json.dump(result, f, ensure_ascii=False, indent=2)
os.replace(tmp_path, str(path))
except Exception:
Expand Down
41 changes: 34 additions & 7 deletions scripts/kanban_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,14 @@ def _sanitize_remark(raw):
return _sanitize_text(raw, 120)


def _todo_counts(task):
"""返回 (completed, total) 便于完成态校验。"""
todos = task.get('todos') or []
total = len(todos)
completed = sum(1 for td in todos if td.get('status') == 'completed')
return completed, total


def _infer_agent_id_from_runtime(task=None):
"""尽量推断当前执行该命令的 Agent。"""
for k in ('OPENCLAW_AGENT_ID', 'OPENCLAW_AGENT', 'AGENT_ID'):
Expand Down Expand Up @@ -429,18 +437,33 @@ def modifier(tasks):


def cmd_done(task_id, output_path='', summary=''):
"""标记任务完成(原子操作)"""
"""执行部门回报完成,任务进入 Review 待尚书省汇总审查。"""
rejected = [False]
reject_reason = ['']
def modifier(tasks):
t = find_task(tasks, task_id)
if not t:
log.error(f'任务 {task_id} 不存在')
return tasks
t['state'] = 'Done'
old_state = t.get('state')
if old_state not in ('Doing', 'Next'):
rejected[0] = True
reject_reason[0] = f'当前状态 {old_state} 不允许直接上报完成'
return tasks
completed, total = _todo_counts(t)
if total > 0 and completed < total:
rejected[0] = True
reject_reason[0] = f'todos 未完成({completed}/{total}),禁止直接收口'
return tasks

from_org = t.get('org', '执行部门')
t['state'] = 'Review'
t['org'] = STATE_ORG_MAP.get('Review', t.get('org', ''))
t['output'] = output_path
t['now'] = summary or '任务已完成'
t['now'] = summary or '执行已完成,提交尚书省汇总审查'
t.setdefault('flow_log', []).append({
"at": now_iso(), "from": t.get('org', '执行部门'),
"to": "皇上", "remark": f"✅ 完成:{summary or '任务已完成'}"
"at": now_iso(), "from": from_org,
"to": "尚书省", "remark": f"✅ 执行完成,提交审查:{summary or '待尚书省汇总'}"
})
# 同步设置 outputMeta,避免依赖 refresh_live_data.py 异步补充
if output_path:
Expand All @@ -454,8 +477,12 @@ def modifier(tasks):
return tasks
atomic_json_update(TASKS_FILE, modifier, [])
_trigger_refresh()
log.info(f'✅ {task_id} 已完成')
_append_audit(task_id, _infer_agent_id_from_runtime(), 'done', None, output_path, summary)
if rejected[0]:
log.warning(f'⚠️ {task_id} done 被拒绝:{reject_reason[0]}')
_append_audit(task_id, _infer_agent_id_from_runtime(), 'done_rejected', None, 'Review', reject_reason[0])
return
log.info(f'✅ {task_id} 执行完成,已提交尚书省审查')
_append_audit(task_id, _infer_agent_id_from_runtime(), 'done', None, 'Review', summary or '')


def cmd_block(task_id, reason):
Expand Down
78 changes: 78 additions & 0 deletions tests/test_dashboard_review_action.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
"""Regression tests for dashboard review completion gates."""
from __future__ import annotations

import importlib.util
import json
import pathlib
import sys


DASHBOARD_DIR = pathlib.Path(__file__).resolve().parent.parent / "dashboard"
sys.path.insert(0, str(DASHBOARD_DIR))

_SPEC = importlib.util.spec_from_file_location("dashboard_server", DASHBOARD_DIR / "server.py")
dashboard_server = importlib.util.module_from_spec(_SPEC)
assert _SPEC.loader is not None
_SPEC.loader.exec_module(dashboard_server)


def test_review_approve_rejects_incomplete_todos(monkeypatch):
"""Review approve should not close a task when todos are still incomplete."""
tasks = [{
"id": "JJC-REVIEW-001",
"title": "review gate",
"state": "Review",
"org": "尚书省",
"now": "汇总中",
"flow_log": [],
"todos": [
{"id": "1", "title": "已完成", "status": "completed"},
{"id": "2", "title": "未完成", "status": "in-progress"},
],
}]

saved = {}

monkeypatch.setattr(dashboard_server, "load_tasks", lambda: json.loads(json.dumps(tasks, ensure_ascii=False)))
monkeypatch.setattr(
dashboard_server,
"save_tasks",
lambda payload: saved.setdefault("tasks", json.loads(json.dumps(payload, ensure_ascii=False))),
)

result = dashboard_server.handle_review_action("JJC-REVIEW-001", "approve", "试图提前完结")

assert result["ok"] is False
assert "2/2" not in result["error"]
assert "不能直接准奏完结" in result["error"]
assert "tasks" not in saved


def test_review_approve_allows_complete_todos(monkeypatch):
"""Review approve may finish a task once all todos are completed."""
tasks = [{
"id": "JJC-REVIEW-002",
"title": "review gate ok",
"state": "Review",
"org": "尚书省",
"now": "汇总中",
"flow_log": [],
"todos": [
{"id": "1", "title": "已完成", "status": "completed"},
{"id": "2", "title": "已完成2", "status": "completed"},
],
}]

saved = {}

monkeypatch.setattr(dashboard_server, "load_tasks", lambda: json.loads(json.dumps(tasks, ensure_ascii=False)))
monkeypatch.setattr(
dashboard_server,
"save_tasks",
lambda payload: saved.setdefault("tasks", json.loads(json.dumps(payload, ensure_ascii=False))),
)

result = dashboard_server.handle_review_action("JJC-REVIEW-002", "approve", "全部完成")

assert result["ok"] is True
assert saved["tasks"][0]["state"] == "Done"
Loading
Loading