mirror of
https://github.com/wasrusgen/wasrusgen1-crm.git
synced 2026-06-03 14:24:47 +00:00
feat: организационный слой — оргструктура + должностные инструкции (между IDEF0 и ТЗ)
- backend: генераторы build-orgchart (из IDEF0 mechanisms) и build-jobs
(должностные с учётом отклонений клиента), артефакты orgchart/jobs в state
- кабинет: вкладка «🏢 Организация» на этапе 4 — оргструктура (роли, штат,
подчинённость, узкие места) + должностные (ответственность, KPI, полномочия)
This commit is contained in:
parent
940cf3484f
commit
149f02da37
@ -464,6 +464,89 @@ def ask():
|
||||
con.commit()
|
||||
return jsonify({"reply": reply, "deviation_recorded": recorded})
|
||||
|
||||
# ── Организационный слой: оргструктура + должностные инструкции (между IDEF0 и ТЗ) ──
|
||||
ORGCHART_TOOL = {
|
||||
"name": "build_orgchart",
|
||||
"description": "Строит целевую оргструктуру (TO-BE) из функциональной модели и интервью.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"units": {"type": "array", "items": {"type": "object", "properties": {
|
||||
"role": {"type": "string", "description": "Должность/роль"},
|
||||
"reports_to": {"type": "string", "description": "Кому подчиняется (роль) или '—' для верхнего уровня"},
|
||||
"headcount": {"type": "integer", "description": "Сколько человек на этой роли"},
|
||||
"owns_functions": {"type": "array", "items": {"type": "string"}, "description": "Функции IDEF0 (node_id или название), за которые отвечает"},
|
||||
"note": {"type": "string", "description": "Комментарий: совмещения, особенности, конфликт интересов"}
|
||||
}, "required": ["role", "reports_to", "headcount"]}},
|
||||
"insight": {"type": "string", "description": "Ключевой вывод: узкие места, перегруз, конфликты интересов в структуре"}
|
||||
},
|
||||
"required": ["units", "insight"]
|
||||
}
|
||||
}
|
||||
ORG_INSTRUCTION = """Построй целевую оргструктуру (TO-BE) бизнеса клиента.
|
||||
Опирайся на функциональную модель (mechanisms — кто выполняет функции) и интервью.
|
||||
Учитывай реальный масштаб — не раздувай штат. Где есть совмещения ролей или конфликт интересов (один человек и исполняет, и контролирует) — отметь в note соответствующей роли и в insight.
|
||||
Если зафиксированы отклонения клиента — учитывай их (например, совмещение склада и пошива оставлено по требованию клиента).
|
||||
Вызови build_orgchart."""
|
||||
|
||||
@app.route("/api/build-orgchart", methods=["POST"])
|
||||
def build_orgchart_route():
|
||||
data = request.get_json(force=True) or {}
|
||||
proj = get_project(data.get("token"))
|
||||
if not proj:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
extra = stage_artifact_context(proj["id"], "idef0") # canvas + idef0 + отклонения
|
||||
result, usage = run_tool(proj["id"], ORGCHART_TOOL, "build_orgchart", ORG_INSTRUCTION, extra_context=extra, max_tokens=2500)
|
||||
if result is None:
|
||||
return jsonify({"error": usage}), 500
|
||||
save_artifact(proj["id"], "orgchart", result)
|
||||
return jsonify({"orgchart": result, "usage": usage})
|
||||
|
||||
JOBS_TOOL = {
|
||||
"name": "build_job_descriptions",
|
||||
"description": "Должностные инструкции по ролям из оргструктуры и функций, с учётом отклонений клиента.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roles": {"type": "array", "items": {"type": "object", "properties": {
|
||||
"role": {"type": "string"},
|
||||
"purpose": {"type": "string", "description": "Цель должности одним предложением"},
|
||||
"responsibilities": {"type": "array", "items": {"type": "string"}, "description": "Зоны ответственности"},
|
||||
"kpis": {"type": "array", "items": {"type": "string"}, "description": "Показатели эффективности (измеримые)"},
|
||||
"reports_to": {"type": "string", "description": "Кому подчиняется"},
|
||||
"authority": {"type": "array", "items": {"type": "string"}, "description": "Права и полномочия"},
|
||||
"deviation_note": {"type": "string", "description": "Если роль затронута отклонением клиента — как именно (совмещение и риск)"}
|
||||
}, "required": ["role", "purpose", "responsibilities", "kpis"]}}
|
||||
},
|
||||
"required": ["roles"]
|
||||
}
|
||||
}
|
||||
JOBS_INSTRUCTION = """Составь должностные инструкции по ролям из целевой оргструктуры и функциональной модели.
|
||||
Для каждой роли: цель должности, зоны ответственности, измеримые KPI, кому подчиняется, права/полномочия.
|
||||
ВАЖНО: если роль затронута отклонением клиента (совмещение функций и т.п.) — отрази это честно в deviation_note и в обязанностях, с оговоркой про риск (например, совмещение склада и пошива — риск «плавающего» учёта остатков).
|
||||
Вызови build_job_descriptions."""
|
||||
|
||||
@app.route("/api/build-jobs", methods=["POST"])
|
||||
def build_jobs_route():
|
||||
data = request.get_json(force=True) or {}
|
||||
proj = get_project(data.get("token"))
|
||||
if not proj:
|
||||
return jsonify({"error": "not found"}), 404
|
||||
pid = proj["id"]
|
||||
parts = []
|
||||
org = latest_artifact(pid, "orgchart")
|
||||
if org:
|
||||
parts.append("ЦЕЛЕВАЯ ОРГСТРУКТУРА:\n" + json.dumps(org, ensure_ascii=False)[:3000])
|
||||
art = stage_artifact_context(pid, "idef0") # idef0 + canvas + отклонения
|
||||
if art:
|
||||
parts.append(art)
|
||||
extra = "\n\n".join(parts) if parts else None
|
||||
result, usage = run_tool(pid, JOBS_TOOL, "build_job_descriptions", JOBS_INSTRUCTION, extra_context=extra, max_tokens=3500)
|
||||
if result is None:
|
||||
return jsonify({"error": usage}), 500
|
||||
save_artifact(pid, "jobs", result)
|
||||
return jsonify({"jobs": result, "usage": usage})
|
||||
|
||||
# ── Tool schema: строгий IDEF0 (ICOM + декомпозиция) ──
|
||||
ARROW = {
|
||||
"type": "object",
|
||||
@ -1347,6 +1430,8 @@ def get_project_state(token):
|
||||
"model": json.loads(model_row["blocks_json"]) if model_row else None,
|
||||
"selection": latest_artifact(proj["id"], "selection"),
|
||||
"canvas": latest_artifact(proj["id"], "canvas"),
|
||||
"orgchart": latest_artifact(proj["id"], "orgchart"),
|
||||
"jobs": latest_artifact(proj["id"], "jobs"),
|
||||
"spec": latest_artifact(proj["id"], "spec"),
|
||||
"approvals": latest_artifact(proj["id"], "approvals") or {},
|
||||
"crm": crm,
|
||||
|
||||
@ -598,7 +598,7 @@ async function sendMsg(){
|
||||
let anTab="canvas";
|
||||
function renderAnalysis(){
|
||||
const pad=document.getElementById("anPad");
|
||||
pad.innerHTML=`<div class="an-tabs"><div class="an-tab ${anTab==='canvas'?'active':''}" onclick="setAnTab('canvas')">📊 Стратегия</div><div class="an-tab ${anTab==='idef0'?'active':''}" onclick="setAnTab('idef0')">🔧 Функции</div></div><div id="anContent"></div>`;
|
||||
pad.innerHTML=`<div class="an-tabs"><div class="an-tab ${anTab==='canvas'?'active':''}" onclick="setAnTab('canvas')">📊 Стратегия</div><div class="an-tab ${anTab==='idef0'?'active':''}" onclick="setAnTab('idef0')">🔧 Функции</div><div class="an-tab ${anTab==='org'?'active':''}" onclick="setAnTab('org')">🏢 Организация</div></div><div id="anContent"></div>`;
|
||||
renderAnContent();
|
||||
}
|
||||
function setAnTab(t){anTab=t;renderAnalysis()}
|
||||
@ -607,11 +607,49 @@ function renderAnContent(){
|
||||
if(anTab==='canvas'){
|
||||
if(!state.canvas){c.innerHTML=runCard("canvas","📊","Стратегическая модель","Елена построит Business Model Canvas — как устроен ваш бизнес и как он зарабатывает.","Построить стратегию →");return}
|
||||
c.innerHTML=renderCanvas(state.canvas);
|
||||
}else{
|
||||
}else if(anTab==='idef0'){
|
||||
if(!state.model){c.innerHTML=runCard("model","🔧","Функциональная модель","Елена разложит бизнес на функции (IDEF0): входы, выходы, нормы, ресурсы и разрывы.","Построить модель →");return}
|
||||
c.innerHTML=renderIdef(state.model);
|
||||
}else{
|
||||
c.innerHTML=renderOrg();
|
||||
}
|
||||
}
|
||||
function renderOrg(){
|
||||
if(!state.model)return runCard(null,"⚠️","Сначала функции","Оргструктура строится из функциональной модели. Постройте модель на вкладке «Функции».","→ К функциям",()=>setAnTab('idef0'));
|
||||
let h='';
|
||||
if(!state.orgchart)h+=runCard("orgchart","🏢","Целевая оргструктура","Елена построит оргструктуру из модели: кто за что отвечает, подчинённость, штат, узкие места.","Построить оргструктуру →");
|
||||
else h+=renderOrgChart(state.orgchart);
|
||||
if(state.orgchart){
|
||||
if(!state.jobs)h+=`<div style="height:14px"></div>`+runCard("jobs","📋","Должностные инструкции","По ролям: зоны ответственности, KPI, полномочия. С учётом ваших пожеланий (отклонений).","Собрать инструкции →");
|
||||
else h+=`<div style="height:14px"></div>`+renderJobs(state.jobs);
|
||||
}
|
||||
return h;
|
||||
}
|
||||
function renderOrgChart(o){
|
||||
const units=o.units||[];
|
||||
let h=`<div style="background:var(--sb);color:#fff;border-radius:11px;padding:12px 16px;margin-bottom:14px;font-size:13px"><b style="color:var(--mid)">Вывод:</b> ${esc(o.insight||'')}</div>`;
|
||||
h+=units.map(u=>`<div style="background:var(--white);border:1px solid var(--border);border-radius:11px;padding:12px 14px;margin-bottom:8px">
|
||||
<div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap">
|
||||
<span style="font-size:14px;font-weight:800">${esc(u.role||'')}</span>
|
||||
<span style="font-size:11px;font-weight:700;color:#047857;background:#D1FAE5;padding:1px 8px;border-radius:5px">${(+u.headcount||1)} чел.</span>
|
||||
${u.reports_to&&u.reports_to!=='—'?`<span style="font-size:11px;color:#9ca3af">↑ ${esc(u.reports_to)}</span>`:'<span style="font-size:10px;font-weight:700;color:#6366F1;background:#EEF2FF;padding:1px 8px;border-radius:5px">руководство</span>'}
|
||||
</div>
|
||||
${(u.owns_functions&&u.owns_functions.length)?`<div style="font-size:11px;color:#6b7280;margin-top:6px">Отвечает: ${u.owns_functions.map(f=>`<span style="background:#F3F4F6;padding:1px 6px;border-radius:4px;margin-right:3px;display:inline-block">${esc(f)}</span>`).join('')}</div>`:''}
|
||||
${u.note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:6px">⚠ ${esc(u.note)}</div>`:''}
|
||||
</div>`).join('');
|
||||
return h;
|
||||
}
|
||||
function renderJobs(j){
|
||||
const roles=j.roles||[];
|
||||
return `<div style="font-size:13px;font-weight:800;margin-bottom:10px">📋 Должностные инструкции</div>`+roles.map(r=>`<div style="background:var(--white);border:1px solid var(--border);border-radius:11px;padding:14px 16px;margin-bottom:10px">
|
||||
<div style="font-size:14px;font-weight:800">${esc(r.role||'')}</div>
|
||||
<div style="font-size:12px;color:#6b7280;margin:3px 0 9px">${esc(r.purpose||'')}${r.reports_to?` · ↑ ${esc(r.reports_to)}`:''}</div>
|
||||
${(r.responsibilities&&r.responsibilities.length)?`<div style="font-size:11px;font-weight:700;color:#374151;margin-bottom:3px">Зоны ответственности</div><ul style="margin:0 0 9px;padding-left:18px;font-size:12px;color:#4B5563">${r.responsibilities.map(x=>`<li>${esc(x)}</li>`).join('')}</ul>`:''}
|
||||
${(r.kpis&&r.kpis.length)?`<div style="font-size:11px;font-weight:700;color:#047857;margin-bottom:3px">KPI</div><div style="display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px">${r.kpis.map(k=>`<span style="font-size:11px;background:#ECFDF5;color:#047857;padding:2px 8px;border-radius:5px">${esc(k)}</span>`).join('')}</div>`:''}
|
||||
${(r.authority&&r.authority.length)?`<div style="font-size:11px;color:#6b7280">Полномочия: ${r.authority.map(a=>esc(a)).join(' · ')}</div>`:''}
|
||||
${r.deviation_note?`<div style="font-size:11px;color:#92400E;background:#FEF3C7;border-radius:6px;padding:5px 9px;margin-top:8px">⚠ Учтено пожелание клиента: ${esc(r.deviation_note)}</div>`:''}
|
||||
</div>`).join('');
|
||||
}
|
||||
function renderSpecPane(){
|
||||
const pad=document.getElementById("specPad");
|
||||
if(!state.spec){
|
||||
@ -703,7 +741,7 @@ function runCard(stage,ic,t,d,btn,custom){
|
||||
const onclick=custom?'':`onclick="runBuild('${stage}')"`;
|
||||
return `<div class="run-card"><div class="run-ic">${ic}</div><div class="run-t">${t}</div><div class="run-d">${d}</div><button class="btn btn-p" ${id} ${custom?'':onclick}>${btn}</button></div>`;
|
||||
}
|
||||
const BUILD={canvas:["build-canvas","canvas"],model:["build-model","model"],spec:["build-spec","spec"]};
|
||||
const BUILD={canvas:["build-canvas","canvas"],model:["build-model","model"],spec:["build-spec","spec"],orgchart:["build-orgchart","orgchart"],jobs:["build-jobs","jobs"]};
|
||||
async function runBuild(stage){
|
||||
const [ep,key]=BUILD[stage];const btn=document.getElementById(`rb-${stage}`);
|
||||
if(btn){btn.disabled=true;btn.innerHTML=`<span class="spin">⏳</span> Елена анализирует...`}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user