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:
+71
-11
@@ -163,7 +163,7 @@ async function pageProjects() {
|
||||
${list.map(p => `
|
||||
<div class="card proj-card" onclick="location.hash='#/project/${p.id}'">
|
||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||
<b>${esc(p.name)}</b><span class="badge st-${p.status === 'active' ? 'running' : p.status === 'done' ? 'done' : p.status === 'archived' ? 'cancelled' : 'todo'}">${{active:'进行中', planning:'规划中', done:'已完成', archived:'已归档'}[p.status]}</span>
|
||||
<b>${esc(p.name)}</b><span class="badge st-${p.status === 'active' ? 'running' : p.status === 'review' ? 'review' : p.status === 'done' ? 'done' : p.status === 'archived' ? 'cancelled' : 'todo'}">${{active:'进行中', review:'待验收', planning:'规划中', done:'已完成', archived:'已归档'}[p.status]}</span>
|
||||
</div>
|
||||
<div style="color:var(--muted);font-size:12px;margin-top:8px;min-height:32px">${esc(p.objective || p.description || '暂无描述')}</div>
|
||||
<div style="margin-top:10px;font-size:12px;color:var(--muted)">任务 ${p.task_count} · 完成 ${p.done_count}
|
||||
@@ -187,7 +187,7 @@ function openProjectModal(p = {}) {
|
||||
<label>项目描述</label><textarea id="pm-desc">${esc(p.description || '')}</textarea>
|
||||
<div class="row">
|
||||
<div><label>状态</label><select id="pm-status">
|
||||
${['planning','active','done','archived'].map(s => `<option value="${s}" ${p.status === s ? 'selected' : ''}>${{planning:'规划中', active:'进行中', done:'已完成', archived:'已归档'}[s]}</option>`).join('')}
|
||||
${['planning','active','review','done','archived'].map(s => `<option value="${s}" ${p.status === s ? 'selected' : ''}>${{planning:'规划中', active:'进行中', review:'待验收', done:'已完成', archived:'已归档'}[s]}</option>`).join('')}
|
||||
</select></div>
|
||||
<div><label>预算上限(元,0=不限)</label><input id="pm-budget" type="number" value="${p.budget_limit || 0}"></div>
|
||||
</div>
|
||||
@@ -277,8 +277,8 @@ async function pageProject([pid, tab]) {
|
||||
const p = (await api(`/api/projects/${pid}`)).data;
|
||||
const tasks = (await api(`/api/tasks?project_id=${pid}`)).data;
|
||||
const workers = (await api('/api/workers')).data;
|
||||
const autoLogs = (await api(`/api/projects/${pid}/autologs`)).data;
|
||||
projCtx = {pid, p, tasks, workers, tab: tab || 'kanban', autoLogs};
|
||||
const events = (await api(`/api/projects/${pid}/events`)).data;
|
||||
projCtx = {pid, p, tasks, workers, tab: tab || 'kanban', events};
|
||||
renderProjectShell();
|
||||
renderActiveTab();
|
||||
startProjAutoPoll();
|
||||
@@ -291,7 +291,11 @@ function renderActiveTab() {
|
||||
else renderKanban();
|
||||
}
|
||||
|
||||
/* V3.3:AI 主管自动开工 —— 轮询刷新状态/动态/看板 */
|
||||
/* V3.3:AI 主管自动开工 —— 轮询刷新状态/事件/看板 */
|
||||
async function refreshProjEvents() {
|
||||
try { projCtx.events = (await api(`/api/projects/${projCtx.pid}/events`)).data; } catch (e) {}
|
||||
}
|
||||
|
||||
function startProjAutoPoll() {
|
||||
clearInterval(projAutoTimer);
|
||||
renderAutoPanel();
|
||||
@@ -301,7 +305,7 @@ function startProjAutoPoll() {
|
||||
const wasRunning = projCtx.p.auto_status === 'running';
|
||||
const p = (await api(`/api/projects/${projCtx.pid}`)).data;
|
||||
projCtx.p = p;
|
||||
projCtx.autoLogs = (await api(`/api/projects/${projCtx.pid}/autologs`)).data;
|
||||
await refreshProjEvents();
|
||||
const running = p.auto_status === 'running';
|
||||
// 状态变化(开工结束/中断)或运行中:都刷新一次任务视图
|
||||
if (running || wasRunning) {
|
||||
@@ -320,12 +324,43 @@ async function autoStartProj() {
|
||||
toast('🚀 AI 主管已开工,正在拆解派活…', 'ok');
|
||||
const p = (await api(`/api/projects/${projCtx.pid}`)).data;
|
||||
projCtx.p = p;
|
||||
projCtx.autoLogs = (await api(`/api/projects/${projCtx.pid}/autologs`)).data;
|
||||
await refreshProjEvents();
|
||||
renderAutoPanel();
|
||||
startProjAutoPoll();
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
|
||||
/* V3.4:负责人验收(平台内) */
|
||||
function openReviewModal() {
|
||||
const {p} = projCtx;
|
||||
openModal(`
|
||||
<h3>🔔 项目验收:${esc(p.name)}<button class="close" data-close>×</button></h3>
|
||||
<p style="color:var(--muted);font-size:13px">AI 团队已全部完工,由您(负责人)最终验收。<br>验收通过后项目才算正式完成,将自动打包交付。</p>
|
||||
${p.demo_url ? `<p><a href="${esc(p.demo_url)}" target="_blank" style="color:var(--accent)">👀 查看交付 Demo</a></p>` : ''}
|
||||
<label>验收意见(打回必填)</label><textarea id="rv-reason" placeholder="填写验收意见…"></textarea>
|
||||
<div class="modal-actions">
|
||||
<button class="btn" data-close>取消</button>
|
||||
<button class="btn danger" onclick="submitProjectReview('reject')">⛔ 打回返工</button>
|
||||
<button class="btn primary" onclick="submitProjectReview('approve')">✅ 验收通过</button>
|
||||
</div>`);
|
||||
}
|
||||
|
||||
async function submitProjectReview(action) {
|
||||
const reason = $('#rv-reason').value.trim();
|
||||
if (action === 'reject' && !reason) return toast('打回必须填写原因', 'err');
|
||||
try {
|
||||
await api(`/api/projects/${projCtx.pid}/review`, {method: 'POST', body: {action, reason}});
|
||||
closeModal();
|
||||
toast(action === 'approve' ? '✅ 验收通过,项目完成并交付' : '⛔ 已打回,可重新开工返工', 'ok');
|
||||
const p = (await api(`/api/projects/${projCtx.pid}`)).data;
|
||||
projCtx.p = p;
|
||||
await refreshProjEvents();
|
||||
renderProjectShell();
|
||||
renderActiveTab();
|
||||
renderAutoPanel();
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
|
||||
function renderAutoPanel() {
|
||||
const el = $('#auto-panel');
|
||||
if (!el || !projCtx) return;
|
||||
@@ -334,7 +369,15 @@ function renderAutoPanel() {
|
||||
const st = {none:['未开工','todo'], running:['🚀 AI 主管工作中','running'], done:['✅ 自动开工完成','done'], failed:['⚠️ 开工中断','failed']}[p.auto_status] || ['—','todo'];
|
||||
const mgr = p.manager_worker ? esc(p.manager_worker.name) : '<span style="color:var(--warn)">未设置</span>';
|
||||
const team = (p.team_workers || []).map(w => `<span class="pill" style="color:var(--accent)">${esc(w.name)}</span>`).join(' ') || '<span style="color:var(--warn)">未设置</span>';
|
||||
const logs = (projCtx.autoLogs || []).map(l => `<div class="al-item"><span class="al-lv">${l.level === 'error' ? '❌' : l.level === 'warn' ? '⚠️' : l.level === 'success' ? '✅' : '🤖'}</span><span class="al-msg">${esc(l.message)}</span><span class="al-time">${fmtTime(l.created_at)}</span></div>`).join('');
|
||||
const evs = projCtx.events || [];
|
||||
const collapsed = localStorage.getItem('aw_ev_collapsed_' + projCtx.pid) === '1';
|
||||
const icon = l => ({error:'❌', warn:'⚠️', success:'✅', info:'🤖'}[l] || '•');
|
||||
const items = evs.map(e => `
|
||||
<div class="al-item ${e.task_id ? 'al-task' : ''}" ${e.task_id ? `onclick="openTaskDetail(${e.task_id})" title="点击定位任务 #${e.task_id}"` : ''}>
|
||||
<span class="al-lv">${icon(e.level)}</span>
|
||||
<span class="al-msg">${esc(e.message)}${e.task_id ? `<span class="al-go">任务 #${e.task_id} ›</span>` : ''}</span>
|
||||
<span class="al-time">${fmtTime(e.created_at)}</span>
|
||||
</div>`).join('') || '<div class="empty" style="padding:12px">暂无事件</div>';
|
||||
el.innerHTML = `
|
||||
<div class="auto-bar">
|
||||
<span class="badge st-${st[1]}">${st[0]}</span>
|
||||
@@ -342,12 +385,29 @@ function renderAutoPanel() {
|
||||
${running ? '<span class="spin"></span>' : ''}
|
||||
</div>
|
||||
<div class="auto-meta">👔 AI 主管:${mgr} | 👷 干活团队:${team} | ⏱ ${p.auto_started_at ? '开工于 ' + fmtTime(p.auto_started_at) : '—'}</div>
|
||||
${logs ? `<div class="auto-logs">${logs}</div>` : ''}`;
|
||||
<div class="ev-panel ${collapsed ? 'collapsed' : ''}">
|
||||
<div class="ev-head" onclick="toggleEvents()">
|
||||
<span class="ev-arrow">${collapsed ? '▸' : '▾'}</span>
|
||||
<span>📋 事件记录</span>
|
||||
<span class="ev-count">${evs.length} 条</span>
|
||||
<span class="ev-hint">${collapsed ? '点击展开' : '点击收起'}</span>
|
||||
</div>
|
||||
<div class="ev-body">${items}</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function toggleEvents() {
|
||||
const panel = $('#auto-panel .ev-panel');
|
||||
if (!panel) return;
|
||||
const collapsed = panel.classList.contains('collapsed');
|
||||
localStorage.setItem('aw_ev_collapsed_' + projCtx.pid, collapsed ? '0' : '1');
|
||||
renderAutoPanel();
|
||||
}
|
||||
|
||||
function renderProjectShell() {
|
||||
const {p, pid, tab} = projCtx;
|
||||
const running = p.auto_status === 'running';
|
||||
const inReview = p.status === 'review';
|
||||
$('#main').innerHTML = `
|
||||
<div class="toolbar">
|
||||
<a class="btn" href="#/projects">← 返回</a>
|
||||
@@ -355,7 +415,7 @@ function renderProjectShell() {
|
||||
${p.perm === 'view' ? '' : `<button class="btn" onclick="openProjectModal(${JSON.stringify(p).replace(/"/g, '"')})">编辑项目</button>`}
|
||||
${p.perm === 'view' ? '' : `<button class="btn" onclick="openWbsModal()">🧠 AI 规划(WBS)</button>`}
|
||||
${p.perm === 'view' ? '' : `<button class="btn" onclick="openTaskModal()">+ 新建任务</button>`}
|
||||
${p.perm === 'view' || running ? '' : `<button class="btn primary" onclick="autoStartProj()">🚀 AI 主管开工</button>`}
|
||||
${p.perm === 'view' || running ? '' : (inReview ? `<button class="btn primary" onclick="openReviewModal()">🔔 验收项目</button>` : `<button class="btn primary" onclick="autoStartProj()">🚀 AI 主管开工</button>`)}
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<a href="#/project/${pid}/kanban" class="${tab === 'kanban' ? 'active' : ''}">看板</a>
|
||||
@@ -2542,7 +2602,7 @@ async function entGrantUser(uid, uname) {
|
||||
<div class="sec-title" style="margin-top:14px">📁 项目授权(${projects.length} 个)</div>
|
||||
<table style="max-height:220px;overflow:auto;display:block"><tbody>
|
||||
${projects.map(p => `
|
||||
<tr><td>#${p.id} ${esc(p.name)} <span class="tag">${{active:'进行中',planning:'规划中',done:'已完成',archived:'已归档'}[p.status] || p.status}</span></td>
|
||||
<tr><td>#${p.id} ${esc(p.name)} <span class="tag">${{active:'进行中',review:'待验收',planning:'规划中',done:'已完成',archived:'已归档'}[p.status] || p.status}</span></td>
|
||||
<td style="width:140px"><select data-gp="${p.id}">
|
||||
${PROJ_PERMS.map(([v, l]) => `<option value="${v}" ${p.perm === v ? 'selected' : ''}>${l}</option>`).join('')}
|
||||
</select></td></tr>`).join('') || '<tr><td class="empty">暂无项目</td></tr>'}
|
||||
|
||||
+12
-1
@@ -208,9 +208,20 @@ code{background:var(--panel2);border:1px solid var(--border);border-radius:6px;p
|
||||
.pick-item small{color:var(--muted)}
|
||||
.auto-bar{display:flex;align-items:center;gap:10px;background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:10px 14px;margin-bottom:8px}
|
||||
.auto-meta{font-size:12px;color:var(--muted);padding:0 4px 8px}
|
||||
.auto-logs{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:8px 12px;margin-bottom:14px;max-height:180px;overflow-y:auto}
|
||||
.ev-panel{background:var(--panel);border:1px solid var(--border);border-radius:10px;margin-bottom:14px;overflow:hidden}
|
||||
.ev-head{display:flex;align-items:center;gap:8px;padding:10px 14px;cursor:pointer;font-size:13px;font-weight:600;user-select:none}
|
||||
.ev-head:hover{background:var(--panel2)}
|
||||
.ev-arrow{color:var(--accent);font-size:12px;transition:transform .15s}
|
||||
.ev-count{background:var(--panel2);border:1px solid var(--border);border-radius:20px;padding:0 8px;font-size:11px;color:var(--muted)}
|
||||
.ev-hint{margin-left:auto;color:var(--muted);font-size:11px;font-weight:400}
|
||||
.ev-body{max-height:320px;overflow-y:auto;padding:6px 12px 10px;border-top:1px solid var(--border)}
|
||||
.ev-panel.collapsed .ev-body{display:none}
|
||||
.ev-panel.collapsed .ev-arrow{transform:rotate(-90deg)}
|
||||
.al-item{display:flex;gap:8px;align-items:baseline;font-size:12px;padding:4px 0;border-bottom:1px dashed var(--border)}
|
||||
.al-item:last-child{border-bottom:none}
|
||||
.al-lv{flex:none}
|
||||
.al-msg{flex:1;word-break:break-all}
|
||||
.al-time{flex:none;color:var(--muted);font-size:11px}
|
||||
.al-task{cursor:pointer}
|
||||
.al-task:hover .al-msg{color:var(--accent)}
|
||||
.al-go{color:var(--accent);font-size:11px;margin-left:6px;border:1px solid var(--border);border-radius:4px;padding:0 5px}
|
||||
Reference in New Issue
Block a user