V3.3 AI主管自动开工:新建项目必选AI主管+多选干活团队,创建即自动跑起来
- 新建项目必填 AI 主管(manager_worker_id)+ 干活团队(project_team_workers 多选,默认AI牛/AI马),均可随时更换
- 创建后立即自动开工:AI主管WBS拆解→轮询分派团队→DAG自动执行→全程监控(autostart.py)
- 失败任务由AI主管诊断原因并修订指令重试(最多2次),全程记入 project_logs 动态
- 项目页新增「AI主管动态」面板:状态/最新动态/主管与团队/动态流水,运行中每3秒自动刷新看板
- 支持手动「🚀 AI主管开工」:已有任务跳过规划直接执行存量;换主管/团队后按新配置生效
- 收工自动汇总完成数/失败数/待审核数/总成本,触发交付体系通知送达者
- 老库自动迁移:projects 新增 manager_worker_id/auto_status 等列 + project_team_workers/project_logs 表
This commit is contained in:
+113
-8
@@ -95,6 +95,8 @@ const routes = {
|
||||
'enterprise': pageEnterprise
|
||||
};
|
||||
function router() {
|
||||
clearInterval(projAutoTimer); // 离开项目页时停止 AI 主管轮询
|
||||
projAutoTimer = null;
|
||||
const hash = location.hash.replace(/^#\//, '') || 'dashboard';
|
||||
const parts = hash.split('/');
|
||||
const name = parts[0];
|
||||
@@ -176,6 +178,7 @@ async function pageProjects() {
|
||||
|
||||
function openProjectModal(p = {}) {
|
||||
const usersP = api('/api/users');
|
||||
const workersP = api('/api/workers');
|
||||
openModal(`
|
||||
<h3>${p.id ? '编辑项目' : '新建项目'}<button class="close" data-close>×</button></h3>
|
||||
<label>项目名称 *</label><input id="pm-name" value="${esc(p.name || '')}" placeholder="例如:营销内容批量生产">
|
||||
@@ -198,9 +201,17 @@ function openProjectModal(p = {}) {
|
||||
</select></div>
|
||||
</div>
|
||||
<label>交付说明</label><input id="pm-dnote" value="${esc(p.deliver_note || '')}" placeholder="给送达者的交付说明(可选)">
|
||||
<div class="sec-divider">🤖 AI 虚拟团队(创建后自动开工)</div>
|
||||
<label>👔 AI 主管(管理者)* <small style="color:var(--muted)">负责拆解任务、派活、监控,默认「AI主管」</small></label>
|
||||
<select id="pm-mgr"><option value="">加载 Worker 中…</option></select>
|
||||
<label>👷 干活团队(AI 主管支配的 Worker,可多选)* <small style="color:var(--muted)">默认 AI牛 / AI马</small></label>
|
||||
<div id="pm-team" class="pick-list"><div class="empty" style="padding:8px">加载 Worker 中…</div></div>
|
||||
<div class="row">
|
||||
<label style="display:flex;align-items:center;gap:6px;font-weight:normal"><input type="checkbox" id="pm-auto-review"> 任务完成后需人工审核(默认不勾选 = AI 主管全自动跑到完工)</label>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn" data-close>取消</button>
|
||||
<button class="btn primary" id="pm-save">保存</button>
|
||||
<button class="btn primary" id="pm-save">${p.id ? '保存' : '创建并开工 🚀'}</button>
|
||||
</div>`);
|
||||
usersP.then(r => {
|
||||
const sel = $('#pm-email');
|
||||
@@ -211,6 +222,24 @@ function openProjectModal(p = {}) {
|
||||
`<option value="${u.id}" ${String(u.id) === String(cur) ? 'selected' : ''}>${esc(u.display_name || u.username)}(${esc(u.username)} · ${esc(u.email || '无邮箱')})</option>`).join('');
|
||||
if (!cur) sel.value = '';
|
||||
}).catch(() => {});
|
||||
workersP.then(r => {
|
||||
const ws = r.data.filter(w => w.status === 'enabled');
|
||||
if (!ws.length) { $('#pm-mgr').innerHTML = '<option value="">请先在 Worker 管理中注册 AI Worker</option>'; return; }
|
||||
// 默认:AI 主管 = 名字含「主管/经理」的 Worker;干活团队 = 含「牛/马」的(如 AI牛/AI马),否则除主管外全部
|
||||
let curMgr = p.manager_worker ? p.manager_worker.id : (p.manager_worker_id || '');
|
||||
if (!curMgr) {
|
||||
const m = ws.find(w => /主管|经理|manager/i.test(w.name));
|
||||
curMgr = (m || ws[0]).id;
|
||||
}
|
||||
$('#pm-mgr').innerHTML = '<option value="">— 请选择 AI 主管 —</option>' +
|
||||
ws.map(w => `<option value="${w.id}" ${String(w.id) === String(curMgr) ? 'selected' : ''}>${esc(w.name)}(${esc(w.provider)}/${esc(w.model)})</option>`).join('');
|
||||
const curTeam = new Set((p.team_workers || []).map(w => w.id));
|
||||
let defaults = ws.filter(w => /牛|马/.test(w.name) && String(w.id) !== String(curMgr));
|
||||
if (!defaults.length) defaults = ws.filter(w => String(w.id) !== String(curMgr));
|
||||
$('#pm-team').innerHTML = ws.map(w => `
|
||||
<label class="pick-item"><input type="checkbox" value="${w.id}" ${curTeam.has(w.id) || (!p.id && defaults.includes(w)) ? 'checked' : ''}>
|
||||
<span>${esc(w.name)}</span><small>${esc(w.provider)}/${esc(w.model)}</small></label>`).join('');
|
||||
}).catch(() => {});
|
||||
$('#pm-save').addEventListener('click', async () => {
|
||||
const body = {
|
||||
name: $('#pm-name').value.trim(), objective: $('#pm-objective').value,
|
||||
@@ -218,41 +247,115 @@ function openProjectModal(p = {}) {
|
||||
status: $('#pm-status').value, budget_limit: parseFloat($('#pm-budget').value || 0),
|
||||
deliver_user_id: Number($('#pm-email').value || 0),
|
||||
deliver_type: $('#pm-dtype').value,
|
||||
deliver_note: $('#pm-dnote').value.trim()
|
||||
deliver_note: $('#pm-dnote').value.trim(),
|
||||
manager_worker_id: Number($('#pm-mgr').value || 0),
|
||||
team_worker_ids: [...$$('#pm-team input:checked')].map(i => Number(i.value)),
|
||||
auto_review: !!$('#pm-auto-review').checked
|
||||
};
|
||||
if (!body.name) return toast('请填写项目名称', 'err');
|
||||
if (!body.deliver_user_id) return toast('必填:选择送达者用户', 'err');
|
||||
if (!body.manager_worker_id) return toast('必填:选择 AI 主管 Worker', 'err');
|
||||
if (!body.team_worker_ids.length) return toast('必填:至少勾选 1 个干活 AI Worker', 'err');
|
||||
try {
|
||||
await api(p.id ? `/api/projects/${p.id}` : '/api/projects', {method: p.id ? 'PUT' : 'POST', body});
|
||||
toast('已保存', 'ok'); closeModal(); router();
|
||||
if (p.id) {
|
||||
await api(`/api/projects/${p.id}`, {method: 'PUT', body});
|
||||
toast('已保存(新主管/团队将在下次开工时生效)', 'ok'); closeModal(); router();
|
||||
} else {
|
||||
const r = await api('/api/projects', {method: 'POST', body});
|
||||
toast('项目已创建,AI 主管已开工 🚀', 'ok'); closeModal();
|
||||
location.hash = `#/project/${r.id}`;
|
||||
}
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- 项目详情(看板 / DAG / 知识库) ---------- */
|
||||
let projCtx = null; // {pid, p, tasks, workers, tab}
|
||||
/* ---------- 项目详情(看板 / DAG / 知识库 / 交付) ---------- */
|
||||
let projCtx = null; // {pid, p, tasks, workers, tab, autoLogs}
|
||||
let projAutoTimer = null;
|
||||
|
||||
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;
|
||||
projCtx = {pid, p, tasks, workers, tab: tab || 'kanban'};
|
||||
const autoLogs = (await api(`/api/projects/${pid}/autologs`)).data;
|
||||
projCtx = {pid, p, tasks, workers, tab: tab || 'kanban', autoLogs};
|
||||
renderProjectShell();
|
||||
renderActiveTab();
|
||||
startProjAutoPoll();
|
||||
}
|
||||
|
||||
function renderActiveTab() {
|
||||
if (projCtx.tab === 'dag') renderDag();
|
||||
else if (projCtx.tab === 'kb') renderKb();
|
||||
else if (projCtx.tab === 'deliver') renderDeliver();
|
||||
else renderKanban();
|
||||
}
|
||||
|
||||
/* V3.3:AI 主管自动开工 —— 轮询刷新状态/动态/看板 */
|
||||
function startProjAutoPoll() {
|
||||
clearInterval(projAutoTimer);
|
||||
renderAutoPanel();
|
||||
if (projCtx.p.auto_status !== 'running') return;
|
||||
projAutoTimer = setInterval(async () => {
|
||||
try {
|
||||
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;
|
||||
const running = p.auto_status === 'running';
|
||||
// 状态变化(开工结束/中断)或运行中:都刷新一次任务视图
|
||||
if (running || wasRunning) {
|
||||
projCtx.tasks = (await api(`/api/tasks?project_id=${projCtx.pid}`)).data;
|
||||
if (projCtx.tab === 'dag') renderDag(); else renderKanban();
|
||||
}
|
||||
renderAutoPanel();
|
||||
if (!running) { clearInterval(projAutoTimer); projAutoTimer = null; }
|
||||
} catch (e) {}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
async function autoStartProj() {
|
||||
try {
|
||||
await api(`/api/projects/${projCtx.pid}/autostart`, {method: 'POST'});
|
||||
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;
|
||||
renderAutoPanel();
|
||||
startProjAutoPoll();
|
||||
} catch (e) { toast(e.message, 'err'); }
|
||||
}
|
||||
|
||||
function renderAutoPanel() {
|
||||
const el = $('#auto-panel');
|
||||
if (!el || !projCtx) return;
|
||||
const p = projCtx.p;
|
||||
const running = p.auto_status === 'running';
|
||||
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('');
|
||||
el.innerHTML = `
|
||||
<div class="auto-bar">
|
||||
<span class="badge st-${st[1]}">${st[0]}</span>
|
||||
<span style="flex:1;color:var(--muted)">${esc(p.auto_message || '尚未开工:创建项目后自动开工,或点右上角「🚀 AI 主管开工」')}</span>
|
||||
${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>` : ''}`;
|
||||
}
|
||||
|
||||
function renderProjectShell() {
|
||||
const {p, pid, tab} = projCtx;
|
||||
const running = p.auto_status === 'running';
|
||||
$('#main').innerHTML = `
|
||||
<div class="toolbar">
|
||||
<a class="btn" href="#/projects">← 返回</a>
|
||||
<h1 class="page-title" style="margin:0;flex:1">${esc(p.name)}<small>${esc(p.objective || '')}</small></h1>
|
||||
${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 primary" onclick="openTaskModal()">+ 新建任务</button>`}
|
||||
${p.perm === 'view' ? '' : `<button class="btn" onclick="openTaskModal()">+ 新建任务</button>`}
|
||||
${p.perm === 'view' || running ? '' : `<button class="btn primary" onclick="autoStartProj()">🚀 AI 主管开工</button>`}
|
||||
</div>
|
||||
<div class="tabs">
|
||||
<a href="#/project/${pid}/kanban" class="${tab === 'kanban' ? 'active' : ''}">看板</a>
|
||||
@@ -260,7 +363,9 @@ function renderProjectShell() {
|
||||
<a href="#/project/${pid}/kb" class="${tab === 'kb' ? 'active' : ''}">知识库 RAG</a>
|
||||
<a href="#/project/${pid}/deliver" class="${tab === 'deliver' ? 'active' : ''}">📦 交付</a>
|
||||
</div>
|
||||
<div id="auto-panel"></div>
|
||||
<div id="tab-body"></div>`;
|
||||
renderAutoPanel();
|
||||
}
|
||||
|
||||
async function refreshProj() {
|
||||
|
||||
@@ -198,3 +198,19 @@ code{background:var(--panel2);border:1px solid var(--border);border-radius:6px;p
|
||||
.form-grid textarea{resize:vertical}
|
||||
#main .tabs a{cursor:pointer}
|
||||
.tag{display:inline-block;background:var(--accent-soft,#eef2ff);color:#4f5bd5;border-radius:4px;padding:0 6px;font-size:11px;margin-right:4px}
|
||||
|
||||
/* ---------- V3.3 AI 主管自动开工 ---------- */
|
||||
.sec-divider{font-size:12px;font-weight:600;color:var(--accent);margin:16px 0 4px;padding-top:12px;border-top:1px dashed var(--border)}
|
||||
.pick-list{display:flex;flex-wrap:wrap;gap:8px;margin-top:6px}
|
||||
.pick-item{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text);margin:0;padding:6px 12px;background:var(--panel2);border:1px solid var(--border);border-radius:20px;cursor:pointer;font-weight:400}
|
||||
.pick-item:has(input:checked){border-color:var(--accent);background:rgba(79,140,255,.12)}
|
||||
.pick-item input{width:auto}
|
||||
.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}
|
||||
.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}
|
||||
Reference in New Issue
Block a user