V3.4 负责人验收制+邮件修复+事件面板+真实网页交付
- 项目级验收:全部任务完成后进入「待验收」,邮件通知负责人,验收通过才算完成;公开验收链接 /review/<token> 免登录一键通过/打回(打回必填原因) - 打回自动返工:自动生成含负责人意见的返工任务,AI主管立即重做并再次提交验收;平台内也可验收 - 真实网页交付:任务HTML产出自动落盘工作目录(剥离围栏/前置叙述),Demo展示真实页面;返工产出覆盖入口页;老项目已回填 - 事件记录面板:合并AI主管动态+任务日志+交付记录,可展开/收起(记忆状态),任务事件可点击定位 - 邮件通道修复:补 Date/Message-ID 头(amavisd 拒收 invalid header section 根因),打包附件/通知恢复送达;notify.py 改标准 MIME - 规划重试:WBS 拆解失败自动重试3次,仍失败邮件通知负责人 - 项目状态新增 review(待验收),全端展示
This commit is contained in:
+51
-21
@@ -150,6 +150,7 @@ def set_team(pid, worker_ids):
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 规划:AI 主管拆解 WBS → 建任务(轮询分派团队 Worker)
|
||||
# V3.4:解析失败自动重试 3 次(每次重新调用 LLM),仍失败抛出异常由上层通知负责人
|
||||
# ---------------------------------------------------------------------------
|
||||
def plan_tasks(pid, manager, team, review_required=0):
|
||||
"""AI 主管规划并创建任务。返回 (created_ids, plan_message)"""
|
||||
@@ -157,14 +158,23 @@ def plan_tasks(pid, manager, team, review_required=0):
|
||||
goal = (proj.get('objective') or proj.get('name') or '').strip()
|
||||
if not goal:
|
||||
raise ValueError('项目缺少目标(Objective),AI 主管无法规划')
|
||||
r = llm_gateway.chat(
|
||||
manager['provider'], manager['model'],
|
||||
[{'role': 'system', 'content': '你只输出 JSON,不输出任何解释文字。'},
|
||||
{'role': 'user', 'content': WBS_PROMPT.format(
|
||||
goal=goal, accept=proj.get('acceptance_criteria') or '—')}],
|
||||
temperature=0.3, max_tokens=3000,
|
||||
base_url=manager['base_url'] or None, api_key=manager['api_key'] or None)
|
||||
tasks = parse_wbs(r['text'])
|
||||
last_err = None
|
||||
for attempt in range(1, 4):
|
||||
try:
|
||||
r = llm_gateway.chat(
|
||||
manager['provider'], manager['model'],
|
||||
[{'role': 'system', 'content': '你只输出 JSON,不输出任何解释文字。'},
|
||||
{'role': 'user', 'content': WBS_PROMPT.format(
|
||||
goal=goal, accept=proj.get('acceptance_criteria') or '—')}],
|
||||
temperature=0.3, max_tokens=3000 * attempt,
|
||||
base_url=manager['base_url'] or None, api_key=manager['api_key'] or None)
|
||||
tasks = parse_wbs(r['text'])
|
||||
break
|
||||
except Exception as e:
|
||||
last_err = e
|
||||
_log(pid, 'warn', f'任务拆解第 {attempt}/3 次失败({str(e)[:100]}),重新规划中…')
|
||||
else:
|
||||
raise ValueError(f'AI 主管连续 3 次拆解任务失败({str(last_err)[:120]}),已中断并通知负责人')
|
||||
created = []
|
||||
for i, t in enumerate(tasks):
|
||||
dep_idx = t.get('depends_on') or []
|
||||
@@ -270,14 +280,7 @@ def monitor(pid, manager, team, timeout=1800):
|
||||
|
||||
def _finalize(pid, status, message):
|
||||
_set_auto(pid, status, message, finished=True)
|
||||
if status == 'done':
|
||||
try:
|
||||
delivery.auto_complete_if_ready(pid)
|
||||
except Exception:
|
||||
pass
|
||||
_log(pid, 'success', message)
|
||||
else:
|
||||
_log(pid, 'warn', message)
|
||||
_log(pid, 'success' if status == 'done' else 'warn', message)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -327,7 +330,7 @@ def autostart_project(pid, review_required=0):
|
||||
if result == 'timeout':
|
||||
raise ValueError('监控超时,仍有任务未完成(可在任务就绪后点「继续开工」)')
|
||||
|
||||
# 4) 收尾
|
||||
# 4) 收尾:全部任务完成 → 提交负责人验收(AI 无权宣布项目完成)
|
||||
by = {r['status']: r['c'] for r in db.q(
|
||||
'SELECT status, COUNT(*) c FROM tasks WHERE project_id=? AND deleted=0 GROUP BY status', (pid,))}
|
||||
done = by.get('done', 0)
|
||||
@@ -335,16 +338,43 @@ def autostart_project(pid, review_required=0):
|
||||
review = by.get('review', 0)
|
||||
total = sum(by.values())
|
||||
costs = db.q('SELECT COALESCE(SUM(cost),0) c FROM cost_records WHERE project_id=?', (pid,))[0]['c']
|
||||
msg = f'AI 主管收工:{done}/{total} 个任务完成' + (f',{failed} 个失败' if failed else '') + \
|
||||
(f',{review} 个待人工审核' if review else '') + f',总成本 ¥{costs:.4f}'
|
||||
_finalize(pid, 'done' if failed == 0 else 'failed', msg)
|
||||
if failed == 0 and review == 0 and total > 0:
|
||||
# 转待验收:delivery.auto_complete_if_ready → enter_review(发邮件给负责人)
|
||||
try:
|
||||
r = delivery.auto_complete_if_ready(pid)
|
||||
if r and r.get('ok'):
|
||||
msg = f'AI 主管收工:{done}/{total} 个任务完成,已提交负责人验收,总成本 ¥{costs:.4f}'
|
||||
else:
|
||||
msg = f'AI 主管收工:{done}/{total} 个任务完成,但验收通知异常:{((r or {}).get("msg") or "")[:80]}'
|
||||
except Exception as e:
|
||||
msg = f'AI 主管收工:{done}/{total} 个任务完成,验收提交异常:{str(e)[:80]}'
|
||||
_finalize(pid, 'done', msg)
|
||||
else:
|
||||
msg = f'AI 主管收工:{done}/{total} 个任务完成' + (f',{failed} 个失败' if failed else '') + \
|
||||
(f',{review} 个待人工审核' if review else '') + f',总成本 ¥{costs:.4f}'
|
||||
_finalize(pid, 'done' if failed == 0 else 'failed', msg)
|
||||
try:
|
||||
notify.notify('task_done', f'项目自动开工完成:{proj["name"]}', msg, save_alert=False)
|
||||
except Exception:
|
||||
pass
|
||||
except Exception as e:
|
||||
_finalize(pid, 'failed', f'自动开工中断:{str(e)[:200]}')
|
||||
msg = f'自动开工中断:{str(e)[:200]}'
|
||||
_finalize(pid, 'failed', msg)
|
||||
_log(pid, 'error', traceback.format_exc())
|
||||
# V3.4:连续失败/中断 → 邮件通知项目负责人(送达者)
|
||||
try:
|
||||
proj = db.q('SELECT * FROM projects WHERE id=?', (pid,), one=True)
|
||||
if proj:
|
||||
email = delivery.deliver_email_of(proj)
|
||||
if email:
|
||||
delivery.notify_deliverer(
|
||||
proj, f'⚠️ 项目自动开工中断:{proj["name"]}',
|
||||
f'AI 主管自动开工中断:{msg}\n\n'
|
||||
f'请到平台查看项目详情(任务/日志),或稍后在项目页点击「🚀 AI 主管开工」重试。')
|
||||
else:
|
||||
_log(pid, 'warn', '项目未配置送达者邮箱,中断通知无法发送')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
_threads = {}
|
||||
|
||||
Reference in New Issue
Block a user