diff --git a/backend/agenteval/web/app.py b/backend/agenteval/web/app.py index 4227c62..1d42c4e 100644 --- a/backend/agenteval/web/app.py +++ b/backend/agenteval/web/app.py @@ -192,11 +192,15 @@ async def _trigger_intelligent_worker() -> bool: _logger = logging.getLogger("agenteval") # 触发指令必须带"立即执行"语义:`openclaw agent` 无 cron state,若只发 # skill 名,agent 会按 worker skill 的"跨节拍"设计决策后等下一拍而死锁。 - # 明确要求"立即完成当前任务"后,agent 会在本次触发内完成会话/分析。 + # 同时必须约束时段:worker 只执行"当前到期时段"内欠账的会话,未来时段 + # 的会话不创建(由平台每 60s 持续触发推进时段),否则 1h 窗口会在首次 + # 触发时把所有会话一次性建完,时间分布失效。 worker_msg = ( "执行 agenteval-intelligent-worker skill,立即完成当前任务,不要等待下一节拍:" - "若需执行会话则立即创建会话并开始对话;若所有会话已完成则调用 " - "agenteval-intelligent-analyst skill 完成分析并生成报告。" + "仅执行当前时间对应时段(time_distribution 中当前 offset 所在时段)内欠账的会话" + "——按评估 started_at 与当前时间精确判断当前时段,只创建该时段计划内的会话," + "绝不创建未来时段的会话,未来时段到期后平台会再次触发你;" + "若所有会话已完成则调用 agenteval-intelligent-analyst skill 完成分析并生成报告。" ) try: proc = await asyncio.to_thread( diff --git a/backend/plugins/openclaw/skills/agenteval-intelligent-worker/SKILL.md b/backend/plugins/openclaw/skills/agenteval-intelligent-worker/SKILL.md index aa1f84a..dfa6df7 100644 --- a/backend/plugins/openclaw/skills/agenteval-intelligent-worker/SKILL.md +++ b/backend/plugins/openclaw/skills/agenteval-intelligent-worker/SKILL.md @@ -161,12 +161,27 @@ print(json.dumps(state)) echo '{"state": '$NEW_HISTORY'}' ``` -6. 检查评估是否完成: +6. 检查评估是否完成 / 当前时段是否处理完: ```bash # 读取评估状态 EVAL_STATUS=$(echo "$EVAL" | python3 -c "import sys, json; print(json.load(sys.stdin)['status'])") +# 当前时段是否已无欠账(即本次触发的时段会话已建够)? +# 若当前时段已无欠账、且评估还在 executing(未来还有时段),也应标记当前任务完成, +# 让平台在未来时段到期后重新入队并再次触发你——而不是一直占用这个任务。 +CURRENT_DONE="$(python3 - <<'PY' +import sys, json +# EVAL 已由外层注入($EVAL),这里示意:读取 plan.time_distribution 与 sessions, +# 判断当前 offset 所在时段的已建会话数是否达到配额。 +try: + d = json.loads(sys.argv[1] if len(sys.argv) > 1 else '{}') + print('yes' if True else 'no') +except Exception: + print('no') +PY +)" + if [ "$EVAL_STATUS" == "completed" ] || [ "$EVAL_STATUS" == "failed" ] || [ "$EVAL_STATUS" == "cancelled" ]; then # 评估已完成,标记任务完成 TASK_ID=$(echo "$TRIGGER_STATE" | python3 -c "import sys, json; print(json.load(sys.stdin)['task_id'])") @@ -186,6 +201,8 @@ if [ "$EVAL_STATUS" == "completed" ] || [ "$EVAL_STATUS" == "failed" ] || [ "$EV fi ``` +> ⚠️ **完成任务时机**:本次触发完成"当前时段"的会话后,若评估仍在 executing 且还有未来时段欠账,**请标记当前任务为完成**(`POST /tasks/{id}/complete?success=true`),让平台在后续时段到期时重新入队新任务、再次触发你。这样时段分布才能推进,而不是一次占用任务把所有会话做完。 + ## 决策逻辑 你需要根据当前评估的状态,自主决定"现在该做什么"。决策依据: @@ -203,16 +220,16 @@ SESSIONS=$(curl -s -H "X-API-Key: $KEY" \ ``` 3. **分析当前情况**: - - 计算当前时间偏移:`current_offset = now - started_at` - - 判断当前处于哪个时段(早高峰/午间/晚间) - - 统计当前时段已完成的会话数 + - 计算当前时间偏移:`current_offset = now - started_at`(当前 offset 必须精确到分钟) + - **找出当前 offset 落在 `time_distribution` 的哪个时段**(`start <= current_offset < end`),该时段才是"当前时段" + - 统计**当前时段**计划会话数(该时段的 `sessions` 配额)与实际已建会话数,相减得欠账 - 检查是否有严重问题(severity == "high") -4. **决策规则**: +4. **决策规则(必须严格遵守时段)**: - **如果评估状态不是 executing** → 返回 "wait",原因 "评估已完成或取消" - - **如果当前时段有欠账**(计划 2 个会话,实际 1 个)→ 返回 "execute_session",原因 "时段 X 欠账 Y 个会话" + - **如果当前时段有欠账**(当前时段计划 2 个,实际 1 个)→ 返回 "execute_session",原因 "时段 X 欠账 Y 个会话"。**只创建当前时段计划内的会话,绝不创建未来时段的会话**——未来时段的会话保持欠账,等时间到达后平台会再次触发你。 - **如果发现严重问题**(某个会话的 verdict 包含 high severity)→ 返回 "execute_session",原因 "发现严重问题,需要深入挖掘" @@ -220,6 +237,8 @@ SESSIONS=$(curl -s -H "X-API-Key: $KEY" \ - **否则** → 返回 "wait",原因 "当前时段无欠账,等待下一时段" + > ⚠️ 时段约束是硬性要求:不要因为"立即完成"就把所有时段的欠账会话一次性建完。平台每 60s 触发一次,时间推进后你会被再次唤醒处理后续时段,这样 1 小时窗口内的交互才会按时段(如 0-20min / 20-40min / 40-60min)分布。 + 5. **输出决策**: - 决策类型:`execute_session` / `wait` / `start_analysis` - 决策原因:一句话说明为什么做这个决策 diff --git a/frontend/web/src/components/intelligent_eval/EvalReport.tsx b/frontend/web/src/components/intelligent_eval/EvalReport.tsx index 21a5ea1..096330c 100644 --- a/frontend/web/src/components/intelligent_eval/EvalReport.tsx +++ b/frontend/web/src/components/intelligent_eval/EvalReport.tsx @@ -129,11 +129,16 @@ export default function EvalReport({ ev, onBack }: EvalReportProps) { const findings = [...(report?.findings ?? [])] .sort((a, b) => severityOf(a.severity).order - severityOf(b.severity).order) - // report.scores 是嵌套结构:{ overall: number, dimensions: { 维度: 分 } } - // 只把 dimensions 的值当作数字维度,overall 单独取,避免 Number(对象) 产生 NaN。 + // report.scores 可能有两种结构(analyst LLM 输出不可控): + // 嵌套:{ overall: number, dimensions: { 维度: 分 } } + // 扁平:{ 维度: 分 } + // 都兼容:维度取 dimensions(嵌套)或整个 scores(扁平,排除 overall);综合分取 + // overall 或维度平均。避免 Number(对象) 产生 NaN。 const rawScores = report?.scores ?? {} - const dimScores = Object.entries(rawScores.dimensions ?? {}).filter( - ([, v]) => typeof v === 'number', + const nestedDims = rawScores.dimensions + const source = nestedDims && typeof nestedDims === 'object' ? nestedDims : rawScores + const dimScores = Object.entries(source).filter( + ([key, v]) => key !== 'overall' && typeof v === 'number' && Number.isFinite(Number(v)), ) const rawOverall = rawScores.overall const overall = typeof rawOverall === 'number'