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:
2026-08-14 13:33:59 +08:00
parent 3b5c06fdd9
commit 7c4cbf8994
10 changed files with 514 additions and 42 deletions
+51 -21
View File
@@ -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 = {}